1. Overview
In this tutorial, we’ll illustrate how to create an application that delegates user authentication to a third party, as well as to a custom authorization server, using Spring Boot and Spring Security OAuth.
Also, we’ll demonstrate how to extract both Principal and Authorities using Spring’s PrincipalExtractor and AuthoritiesExtractor interfaces.
For an introduction to Spring Security OAuth2 please refer to these articles.
2. Maven Dependencies
To get started, we need to add the spring-security-oauth2-autoconfigure dependency to our pom.xml:
<dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.0.1.RELEASE</version> </dependency>
3. OAuth Authentication using Github
Next, let’s create the security configuration of our application:
@Configuration @EnableOAuth2Sso public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.antMatcher("/**") .authorizeRequests() .antMatchers("/login**") .permitAll() .anyRequest() .authenticated() .and() .formLogin().disable(); } }
In short, we’re saying that anyone can access the /login endpoint and that all other endpoints will require user authentication.
We’ve also annotated our configuration class with @EnableOAuthSso which converts our application into an OAuth client and creates the necessary components for it to behave as such.
While Spring creates most of the components for us by default, we still need to configure some properties:
security.oauth2.client.client-id=89a7c4facbb3434d599d security.oauth2.client.client-secret=9b3b08e4a340bd20e866787e4645b54f73d74b6a security.oauth2.client.access-token-uri=https://github.com/login/oauth/access_token security.oauth2.client.user-authorization-uri=https://github.com/login/oauth/authorize security.oauth2.client.scope=read:user,user:email security.oauth2.resource.user-info-uri=https://api.github.com/user
Instead of dealing with user account management, we’re delegating it to a third party – in this case, Github – thus enabling us to focus on the logic of our application.
4. Extracting Principal and Authorities
When acting as an OAuth client and authenticating users through a third party there are three steps we need to consider:
- User authentication – the user authenticates with the third party
- User authorization – follows authentication, it’s when the user allows our application to perform certain operations on their behalf; this is where scopes come in
- Fetch user data – use the OAuth token we’ve obtained to retrieve user’s data
Once we retrieve the user’s data, Spring is able to automatically create the user’s Principal and Authorities.
While that may be acceptable, more often than not we find ourselves in a scenario where we want to have complete control over them.
To do so, Spring gives us two interfaces we can use to override its default behavior:
- PrincipalExtractor – Interface we can use to provide our custom logic to extract the Principal
- AuthoritiesExtractor – Similar to PrincipalExtractor, but it’s used to customize Authorities extraction instead
By default, Spring provides two components – FixedPrincipalExtractor and FixedAuthoritiesExtractor – that implement these interfaces and have a pre-defined strategy to create them for us.
4.1. Customizing Github’s Authentication
In our case, we’re aware of how Github’s user data looks like and what we can use to tailor them according to our needs.
As such, to override Spring’s default components we just need to create two Beans that also implement these interfaces.
For our application’s Principal we’re simply going to use the user’s Github username:
@Bean public class GithubPrincipalExtractor implements PrincipalExtractor { @Override public Object extractPrincipal(Map<String, Object> map) { return map.get("login"); } }
Depending on our user’s Github subscription – free, or otherwise – we’ll give them a GITHUB_USER_SUBSCRIBED, or a GITHUB_USER_FREE authority:
@Bean public class GithubAuthoritiesExtractor implements AuthoritiesExtractor { List<GrantedAuthority> GITHUB_FREE_AUTHORITIES = AuthorityUtils.commaSeparatedStringToAuthorityList( "GITHUB_USER,GITHUB_USER_FREE"); List<GrantedAuthority> GITHUB_SUBSCRIBED_AUTHORITIES = AuthorityUtils.commaSeparatedStringToAuthorityList( "GITHUB_USER,GITHUB_USER_SUBSCRIBED"); @Override public List<GrantedAuthority> extractAuthorities (Map<String, Object> map) { if (Objects.nonNull(map.get("plan"))) { if (!((LinkedHashMap) map.get("plan")) .get("name") .equals("free")) { return GITHUB_SUBSCRIBED_AUTHORITIES; } } return GITHUB_FREE_AUTHORITIES; } }
4.2. Using a Custom Authorization Server
We can also use our own Authorization Server for our users – instead of relying on a third party.
Despite the authorization server we decide to use, the components we need to customize both Principal and Authorities remain the same: a PrincipalExtractor and an AuthoritiesExtractor.
We just need to be aware of the data returned by the user-info-uri endpoint and use it as we see fit.
Let’s change our application to authenticate our users using the authorization server described in this article:
security.oauth2.client.client-id=SampleClientId security.oauth2.client.client-secret=secret security.oauth2.client.access-token-uri=http://localhost:8081/auth/oauth/token security.oauth2.client.user-authorization-uri=http://localhost:8081/auth/oauth/authorize security.oauth2.resource.user-info-uri=http://localhost:8081/auth/user/me
Now that we’re pointing to our authorization server we need to create both extractors; in this case, our PrincipalExtractor is going to extract the Principal from the Map using the name key:
@Bean public class BaeldungPrincipalExtractor implements PrincipalExtractor { @Override public Object extractPrincipal(Map<String, Object> map) { return map.get("name"); } }
As for authorities, our Authorization Server is already placing them in its user-info-uri‘s data.
As such, we’re going to extract and enrich them:
@Bean public class BaeldungAuthoritiesExtractor implements AuthoritiesExtractor { @Override public List<GrantedAuthority> extractAuthorities (Map<String, Object> map) { return AuthorityUtils .commaSeparatedStringToAuthorityList(asAuthorities(map)); } private String asAuthorities(Map<String, Object> map) { List<String> authorities = new ArrayList<>(); authorities.add("BAELDUNG_USER"); List<LinkedHashMap<String, String>> authz = (List<LinkedHashMap<String, String>>) map.get("authorities"); for (LinkedHashMap<String, String> entry : authz) { authorities.add(entry.get("authority")); } return String.join(",", authorities); } }
5. Conclusion
In this article, we’ve implemented an application that delegates user authentication to a third party, as well as to a custom authorization server, and demonstrated how to customize both Principal and Authorities.
As usual, the implementation of this example can be found over on Github.
When running locally, you can run and test the application at localhost:8082