Spring Security - OAuth2 Core Classes

You don’t need to understand all the OAuth2 classes to use OAuth2.0 login. However, knowing the core OAuth2 classes and internals will help you configure OAuth2 authentication process and take full advantage of Spring Boot’s OAuth2.0 support.

If you are not familiar with OAuth2, see this post OAuth2 to understand OAuth2 concepts first.

OAuth2LoginAuthenticationToken

  • An AbstractAuthenticationToken for OAuth 2.0 Login, which leverages the OAuth 2.0 Authorization Code Grant Flow.
  • OAuth2LoginAuthenticationToken is used in OAuth2LoginAuthenticationFilter for login purpose
  • Javadoc
  • Source code

OAuth2LoginAuthenticationToken contains the following fields

  • principal of type OAuth2User
  • ClientRegistration
  • OAuth2AuthorizationExchange
  • OAuth2AccessToken
  • OAuth2RefreshToken - this field is optional

OAuth2AuthenticationToken

  • An implementation of an AbstractAuthenticationToken that represents an OAuth 2.0 Authentication.
  • OAuth2AuthenticationToken is created in OAuth2LoginAuthenticationFilter
  • When the user login successfully, OAuth2AuthenticationToken is created and stored in SecurityContext
  • Javadoc
  • Source Code

OAuth2AuthenticationToken contains the following fields

  • principal of type OAuth2User
  • authorizedClientRegistrationId

Note that OAuth2AuthenticationToken doesn’t contain AccessToken or RefreshToken, If you need to use AccessToken or RefreshToken, you can get it from OAuth2AuthorizedClient.

OAuth2User and OidcUser

  • OAuth2User is an Interface that extends OAuth2AuthenticatedPrincipal. DefaultOAuth2User is the default implementation
  • OAuth2AuthenticationToken contains OAuth2User.

OAuth2AuthenticatedPrincipal is an AuthenticatedPrincipal that represents the principal associated with an OAuth 2.0 token.

1
2
3
4
5
6
7
8
public interface OAuth2AuthenticatedPrincipal extends AuthenticatedPrincipal {
@Nullable
default <A> A getAttribute(String name) {
return (A) getAttributes().get(name);
}
Map<String, Object> getAttributes();
Collection<? extends GrantedAuthority> getAuthorities();
}

OAuth2User is an interface that extends OAuth2AuthenticatedPrincipal

1
2
3
public interface OAuth2User extends OAuth2AuthenticatedPrincipal {

}

OidcUser extends OAuth2User. It is created when OpenID Connect is used. This class contains additional information including ID Token. ID Token is a Jwt returned only for OpenID Connect authentication.

1
2
3
4
5
public interface OidcUser extends OAuth2User, IdTokenClaimAccessor {
Map<String, Object> getClaims();
OidcUserInfo getUserInfo();
OidcIdToken getIdToken();
}

OAuth2User or OidcUser can be retrieved when @AuthenticationPrincipal annotation is used. it can also be retrieved from OAuth2AuthenticationToken.

1
2
3
4
@GetMapping(value="/")
public String sayHello(@AuthenticationPrincipal OidcUser principal) {
return "Hello, " + principal.getAttribute("name");
}

ClientRegistration

  • ClientRegistration is a representation of a client registration with an OAuth 2.0 or OpenID Connect 1.0 Provider.
  • A client registration holds information, such as client id, client secret, authorization grant type, redirect URI, scope(s), authorization URI, token URI, and other details.
  • It has a Builder static nested class
  • Its registrationId uniquely identifies the ClientRegistration.
  • source code

See the member variables here:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public final class ClientRegistration {
private String registrationId;
private String clientId;
private String clientSecret;
private ClientAuthenticationMethod clientAuthenticationMethod;
private AuthorizationGrantType authorizationGrantType;
private String redirectUriTemplate;
private Set<String> scopes;
private ProviderDetails providerDetails;
private String clientName;

public class ProviderDetails {
private String authorizationUri;
private String tokenUri;
private UserInfoEndpoint userInfoEndpoint;
private String jwkSetUri;
private Map<String, Object> configurationMetadata;

public class UserInfoEndpoint {
private String uri;
private AuthenticationMethod authenticationMethod;
private String userNameAttributeName;

}
}
}

see ClientRegistration Documentation for explaination of each field.

Client Registration can be configured in application.yml file.

spring.security.oauth2.client.registration is the base property prefix for OAuth Client properties

application.yml - example to configure a Google oauth2 ClientRegistration. Here the registrationId is “google”.

1
2
3
4
5
6
7
8
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}

see Spring Boot 2.x Property Mappings for ClientRegistration for all the properties avaialble for client registration

CommonOAuth2Provider preconfigure Client Registration Builders for Google, Github, Facebook and OKTA. That is why you only need to set client ID and client secret for Google ClientRegistration. Scope, authoriztionUri tokenUri etc are preconfigured already. If the provider is not one of them, you need to provide the necessary information.

The common way is to define ClientRegistration in application.yml. Another approach is to manually define ClientRegistration and provide the custom ClientRegistration to ClientRegistrationRepository.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
public class OAuth2LoginConfig {
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(this.googleClientRegistration());
}

private ClientRegistration googleClientRegistration() {
return ClientRegistration.withRegistrationId("google")
.clientId("google-client-id")
.clientSecret("google-client-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
.scope("openid", "profile", "email", "address", "phone")
.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
.tokenUri("https://www.googleapis.com/oauth2/v4/token")
.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
.userNameAttributeName(IdTokenClaimNames.SUB)
.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
.clientName("Google")
.build();
}
}

ClientRegistrationRepository

  • A ClientRegistrationRepository is a repository of ClientRegistration(s).
  • The auto-configuration registers the ClientRegistrationRepository as a @Bean in the ApplicationContext so that it is available for dependency-injection
  • Javadoc for ClientRegistrationRepository
  • Source code

ClientRegistrationRepository Interface

1
2
3
4
5
6
public interface ClientRegistrationRepository {
/**
* Returns the client registration identified by the provided {@code registrationId}, or {@code null} if not found.
*/
ClientRegistration findByRegistrationId(String registrationId);
}

InMemoryClientRegistrationRepository is an in-memory implementation of ClientRegistrationRepository. It uses a UnmodifiableConcurrentMap to store registration information. Spring Security does not provide another implementation.

If you don’t provide a ClientRegistrationRepository bean, Spring Security will create an InMemoryClientRegistrationRepository bean for you.

OAuth2AuthorizedClient

  • A representation of an OAuth 2.0 Authorized Client. A client is considered “authorized” when the End-User (Resource Owner) has granted authorization to the client to access it’s protected resources.
  • OAuth2AuthorizedClient serves the purpose of associating an OAuth2AccessToken (and optional OAuth2RefreshToken) to a ClientRegistration (client) and resource owner, who is the Principal end-user that granted the authorization.
  • You can get the current user’s OAuth2AuthorizedClient using OAuth2AuthorizedClientService.loadAuthorizedClient method.
  • OAuth2AuthorizedClient contains accessToken and an optional refreshToken.
  • Source Code
  • Javadoc

source code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class OAuth2AuthorizedClient implements Serializable {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final ClientRegistration clientRegistration;
private final String principalName;
private final OAuth2AccessToken accessToken;
private final OAuth2RefreshToken refreshToken;

public OAuth2AuthorizedClient(ClientRegistration clientRegistration,
String principalName, OAuth2AccessToken accessToken) {
this(clientRegistration, principalName, accessToken, null);
}

public OAuth2AuthorizedClient(ClientRegistration clientRegistration, String principalName,
OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken) {
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
Assert.hasText(principalName, "principalName cannot be empty");
Assert.notNull(accessToken, "accessToken cannot be null");
this.clientRegistration = clientRegistration;
this.principalName = principalName;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}

public ClientRegistration getClientRegistration() {
return this.clientRegistration;
}

public String getPrincipalName() {
return this.principalName;
}

public OAuth2AccessToken getAccessToken() {
return this.accessToken;
}

public @Nullable OAuth2RefreshToken getRefreshToken() {
return this.refreshToken;
}
}

OAuth2AuthorizedClientRepository

  • Repository for OAuth2AuthorizedClient(s)
  • Implementations of this interface are responsible for the persistence of Authorized Client(s) between requests.
  • Javadoc
  • Source code

OAuth2AuthorizedClientRepository Interface source code

1
2
3
4
5
6
7
8
9
10
11
12
public interface OAuth2AuthorizedClientRepository {
<T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String clientRegistrationId,
Authentication principal, HttpServletRequest request);


void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal,
HttpServletRequest request, HttpServletResponse response);

void removeAuthorizedClient(String clientRegistrationId, Authentication principal,
HttpServletRequest request, HttpServletResponse response);

}

Two of the implementations are

  • AuthenticatedPrincipalOAuth2AuthorizedClientRepository - the default repository to use. see details below
  • HttpSessionOAuth2AuthorizedClientRepository - An implementation of an OAuth2AuthorizedClientRepository that stores OAuth2AuthorizedClient’s in the HttpSession.

AuthenticatedPrincipalOAuth2AuthorizedClientRepository is the default bean to use when no OAuth2AuthorizedClientRepository bean is found

AuthenticatedPrincipalOAuth2AuthorizedClientRepository

  • An implementation of an OAuth2AuthorizedClientRepository that delegates to the provided OAuth2AuthorizedClientService if the current Principal is authenticated, otherwise, to the default (or provided) OAuth2AuthorizedClientRepository if the current request is unauthenticated (or anonymous).

AuthenticatedPrincipalOAuth2AuthorizedClientRepository source code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public final class AuthenticatedPrincipalOAuth2AuthorizedClientRepository implements OAuth2AuthorizedClientRepository {
private final AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
private final OAuth2AuthorizedClientService authorizedClientService;
private OAuth2AuthorizedClientRepository anonymousAuthorizedClientRepository = new HttpSessionOAuth2AuthorizedClientRepository();

public AuthenticatedPrincipalOAuth2AuthorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) {
Assert.notNull(authorizedClientService, "authorizedClientService cannot be null");
this.authorizedClientService = authorizedClientService;
}

public void setAnonymousAuthorizedClientRepository(OAuth2AuthorizedClientRepository anonymousAuthorizedClientRepository) {
Assert.notNull(anonymousAuthorizedClientRepository, "anonymousAuthorizedClientRepository cannot be null");
this.anonymousAuthorizedClientRepository = anonymousAuthorizedClientRepository;
}

public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String clientRegistrationId, Authentication principal, HttpServletRequest request) {
return this.isPrincipalAuthenticated(principal) ? this.authorizedClientService.loadAuthorizedClient(clientRegistrationId, principal.getName()) : this.anonymousAuthorizedClientRepository.loadAuthorizedClient(clientRegistrationId, principal, request);
}

public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal, HttpServletRequest request, HttpServletResponse response) {
if (this.isPrincipalAuthenticated(principal)) {
this.authorizedClientService.saveAuthorizedClient(authorizedClient, principal);
} else {
this.anonymousAuthorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, request, response);
}

}

public void removeAuthorizedClient(String clientRegistrationId, Authentication principal, HttpServletRequest request, HttpServletResponse response) {
if (this.isPrincipalAuthenticated(principal)) {
this.authorizedClientService.removeAuthorizedClient(clientRegistrationId, principal.getName());
} else {
this.anonymousAuthorizedClientRepository.removeAuthorizedClient(clientRegistrationId, principal, request, response);
}

}

private boolean isPrincipalAuthenticated(Authentication authentication) {
return authentication != null && !this.authenticationTrustResolver.isAnonymous(authentication) && authentication.isAuthenticated();
}
}

HttpSessionOAuth2AuthorizedClientRepository

To use HttpSessionOAuth2AuthorizedClientRepository instead of the default, just create the bean.

1
2
3
4
5
6
7
8
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository() {
return new HttpSessionOAuth2AuthorizedClientRepository();
}
}

OAuth2AuthorizedClientService

  • Implementations of this interface are responsible for the management of Authorized Client(s), which provide the purpose of associating an Access Token credential to a Client and Resource Owner, who is the Principal that originally granted the authorization.
  • The default implementation of OAuth2AuthorizedClientService is InMemoryOAuth2AuthorizedClientService
  • Javadoc
  • source code

OAuth2AuthorizedClientService Interface

1
2
3
4
5
6
7
public interface OAuth2AuthorizedClientService {
<T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String clientRegistrationId, String principalName);

void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal);

void removeAuthorizedClient(String clientRegistrationId, String principalName);
}

implementations are

  • InMemoryOAuth2AuthorizedClientService - this is the default
  • JdbcOAuth2AuthorizedClientService

InMemoryOAuth2AuthorizedClientService

  • InMemoryOAuth2AuthorizedClientService is the default implementation of OAuth2AuthorizedClientService
  • It uses ConcurrentHashMap to store OAuth2AuthorizedClient.
  • OAuth2AuthorizedClient is retrieved using OAuth2AuthorizedClientId, which is composed of clientRegistrationId and principalName
  • Javadoc
  • Source Code

InMemoryOAuth2AuthorizedClientService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public final class InMemoryOAuth2AuthorizedClientService implements OAuth2AuthorizedClientService {
private final Map<OAuth2AuthorizedClientId, OAuth2AuthorizedClient> authorizedClients;
private final ClientRegistrationRepository clientRegistrationRepository;

public InMemoryOAuth2AuthorizedClientService(ClientRegistrationRepository clientRegistrationRepository) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
this.clientRegistrationRepository = clientRegistrationRepository;
this.authorizedClients = new ConcurrentHashMap<>();
}

public InMemoryOAuth2AuthorizedClientService(ClientRegistrationRepository clientRegistrationRepository,
Map<OAuth2AuthorizedClientId, OAuth2AuthorizedClient> authorizedClients) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
Assert.notEmpty(authorizedClients, "authorizedClients cannot be empty");
this.clientRegistrationRepository = clientRegistrationRepository;
this.authorizedClients = new ConcurrentHashMap<>(authorizedClients);
}

@Override
@SuppressWarnings("unchecked")
public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String clientRegistrationId, String principalName) {
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
ClientRegistration registration = this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId);
if (registration == null) {
return null;
}
return (T) this.authorizedClients.get(new OAuth2AuthorizedClientId(clientRegistrationId, principalName));
}

@Override
public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal) {
Assert.notNull(authorizedClient, "authorizedClient cannot be null");
Assert.notNull(principal, "principal cannot be null");
this.authorizedClients.put(new OAuth2AuthorizedClientId(authorizedClient.getClientRegistration().getRegistrationId(),
principal.getName()), authorizedClient);
}

@Override
public void removeAuthorizedClient(String clientRegistrationId, String principalName) {
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
ClientRegistration registration = this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId);
if (registration != null) {
this.authorizedClients.remove(new OAuth2AuthorizedClientId(clientRegistrationId, principalName));
}
}
}