Extracting Principal and Authorities using Spring Security OAuth

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:

  1. User authentication – the user authenticates with the third party

  2. 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

  3. 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:

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:

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;
    }
}

Then, we also need to create beans using these classes:

@Configuration
@EnableOAuth2Sso
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // ...

    @Bean
    public PrincipalExtractor githubPrincipalExtractor() {
        return new GithubPrincipalExtractor();
    }

    @Bean
    public AuthoritiesExtractor githubAuthoritiesExtractor() {
        return new GithubAuthoritiesExtractor();
    }
}

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:

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:

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);
    }
}

Then we’ll add the beans to our SecurityConfig class:

@Configuration
@EnableOAuth2Sso
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // ...

    @Bean
    public PrincipalExtractor baeldungPrincipalExtractor() {
        return new BaeldungPrincipalExtractor();
    }

    @Bean
    public AuthoritiesExtractor baeldungAuthoritiesExtractor() {
        return new BaeldungAuthoritiesExtractor();
    }
}

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

Leave a Reply

Your email address will not be published.