These are notes for OAuth2 Login in Spring Security 5 OAuth2 support for OAuth2 Login.
@OAuth2ClientAutoConfiguration
which in turn importsOAuth2ClientRegistrationRepositoryConfiguration
OAuth2WebSecurityConfiguration
is conditioned on spring.security.oauth2.client.*
(OAuth2ClientProperties
) properties being set.
- conditioned on presence of at least one
spring.security.oauth2.client.registration
- By default creates in memory instance of
ClientRegistrationRepository
if one is not already defined based
- conditioned on presence of
ClientRegistrationRepository
- By default creates
- in memory instance of
OAuth2AuthorizedClientService
if one is not already defined based - instance of
AuthenticatedPrincipalOAuth2AuthorizedClientRepository
if oneOAuth2AuthorizedClientRepository
is not already defined based.AuthenticatedPrincipalOAuth2AuthorizedClientRepository
wraps theOAuth2AuthorizedClientService
instance. - Actiavtes
OAuth2SecurityFilterChainConfiguration
if default web security is in place and creates aoauth2SecurityFilterChain
with following defaults:
- in memory instance of
@Bean
SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
http.oauth2Login(withDefaults()); <--- This configures `OAuth2LoginConfigurer`
http.oauth2Client(withDefaults());
return http.build();
}
Notes: If only one ClientRegistration
is present, then the oauth2Login
configuration will directly redirect to OAuth2 provider. If more than one ClientRegistration
is present, then the oauth2Login
configuration will redirect to a page with links to each ClientRegistration
's authorization endpoints. But if other authentication mechanisms are configured such as formLogin()
but the following really controls what happens:
// File: spring-security-config-6.1.1-sources.jar!/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java
240: if (this.defaultEntryPointMappings.size() == 1) {
241: return this.defaultEntryPointMappings.values().iterator().next();
242: }
DelegatingAuthenticationEntryPoint
-> LoginUrlAuthenticationEntryPoint
(This was configured by OAuth2LoginConfigurer
as sshown below) -> RedirectStrategy
are used to redirect to the login page.
This decision is made here: OAuth2LoginConfigurer
's init
method.
Map<String, String> loginUrlToClientName = this.getLoginLinks();
if (loginUrlToClientName.size() == 1) {
// Setup auto-redirect to provider login page
// when only 1 client is configured
this.updateAuthenticationDefaults();
this.updateAccessDefaults(http);
String providerLoginPage = loginUrlToClientName.keySet().iterator().next();
this.registerAuthenticationEntryPoint(http, this.getLoginEntryPoint(http, providerLoginPage));
}
else {
super.init(http); // This will cause the default login page to be rendered with links to each client's authorization endpoint
}
If there are multiple clients then first redirect is to:
http://localhost:8080/login
And with a links to each client's authorization endpoint:
/oauth2/authorization/<client registration id>
e.g.
http://localhost:8080/oauth2/authorization/github
The page is generated by DefaultLoginPageGeneratingFilter
which handles:
if (this.formLoginEnabled) {
}
if (this.oauth2LoginEnabled) {
// Loop over oauth2AuthenticationUrlToClientName
}
if (this.saml2LoginEnabled) {
}
If there is only one client the first redirect is to:
/oauth2/authorization/<client registration id>
This in turn redirects to provider's authorization endpoint.
https://github.com/login/oauth/authorize
with sample payload like:
response_type: code
client_id: 0191ce83c982a47ff19a
scope: read:user
state: nroExruTvZQWGQdKSZDQn_YNshs1rk9Q_QfLZvDTaSc=
redirect_uri: http://localhost:8080/login/oauth2/code/github
File: spring-security-oauth2-client-6.1.1-sources.jar!/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java 99: /** 100: * The default {@code URI} where this {@code Filter} processes authentication 101: * requests. 102: / 103: public static final String DEFAULT_FILTER_PROCESSES_URI = "/login/oauth2/code/";
The common providers are configured in CommonOAuth2Provider
enum e.g.
@Override
public Builder getBuilder(String registrationId) {
ClientRegistration.Builder builder = getBuilder(registrationId,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, DEFAULT_REDIRECT_URL);
builder.scope("read:user");
builder.authorizationUri("https://github.com/login/oauth/authorize");
builder.tokenUri("https://github.com/login/oauth/access_token");
builder.userInfoUri("https://api.github.com/user");
builder.userNameAttributeName("id");
builder.clientName("GitHub");
return builder;
}
DefaultOAuth2AuthorizationRequestResolver
basically creates a OAuth2AuthorizationRequest
for the given URL /oauth2/authorization/github
for a given client github
.
// File: spring-security-oauth2-client-6.1.1-sources.jar!/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java
154: OAuth2AuthorizationRequest.Builder builder = getBuilder(clientRegistration);
155:
156: String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);
157:
158: // @formatter:off
159: builder.clientId(clientRegistration.getClientId())
160: .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
161: .redirectUri(redirectUriStr)
162: .scopes(clientRegistration.getScopes())
163: .state(DEFAULT_STATE_GENERATOR.generateKey());
164: // @formatter:on
Basically builds the redirect URI to provider.
A customizer can set on DefaultOAuth2AuthorizationRequestResolver that can customize OAuth2AuthorizationRequest.Builder
before the request is sent to the provider. This is where state
parameter can be customized. We will use a stragegy such that any instance of Gateway can verify that the state
parameter is valid.
This is where the request is saved in a repository (this is where session comes into picture as the default implementation uses HttpSessionOAuth2AuthorizationRequestRepository
). We will have to use a non session based implementation which can use HTTP-only, secure cookie to store the serialized, encrypted OAuth2AuthorizationRequest
.
// File: spring-security-oauth2-client-6.1.1-sources.jar!/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilter.java
218: if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationRequest.getGrantType())) {
219: this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
220: }
It is retrieved back in OAuth2LoginAuthenticationFilter to finish the code -> access token exchange flow.
File: spring-security-oauth2-client-6.1.1-sources.jar!/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java
169: OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository
170: .removeAuthorizationRequest(request, response);
OAuth2LoginAuthenticationFilter
uses the client to generate the URL for code -> access token exchange.
http://localhost:8080/login/oauth2/code/github
If this is different than the default then you must override e.g.
```properties
spring.security.oauth2.client.registration.github.redirect-uri=http://localhost:8080/login/github
has to match:
oAuth2LoginConfigurer.redirectionEndpoint(redirectionEndpointConfig -> {
redirectionEndpointConfig.baseUri("/login/github");
});
This is where code -> access token exchange happens.
// File: spring-security-oauth2-client-6.1.1-sources.jar!/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java
194: OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this
195: .getAuthenticationManager().authenticate(authenticationRequest);
The following can convert the OAuth2LoginAuthenticationToken
token to OAuth2AuthenticationToken
which is the final result.
// File: spring-security-oauth2-client-6.1.1-sources.jar!/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java
115: private Converter<OAuth2LoginAuthenticationToken, OAuth2AuthenticationToken> authenticationResultConverter = this::createAuthenticationResult;
we will need to override this so that the token has both access token and refresh token.
File: spring-security-oauth2-client-6.1.1-sources.jar!/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java
227: public final void setAuthenticationResultConverter(
oauth2Login.userInfoEndpoint(userInfoEndpointConfig -> {
userInfoEndpointConfig.userService(new OverridingOAuth2UserService());
});
public static class OverridingOAuth2UserService extends DefaultOAuth2UserService {
private static final String MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE = "missing_user_name_attribute";
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint()
.getUserNameAttributeName();
if (!StringUtils.hasText(userNameAttributeName)) {
OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
"Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: "
+ userRequest.getClientRegistration().getRegistrationId(),
null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
JwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(userRequest.getClientRegistration().getProviderDetails().getIssuerUri());
OAuth2AccessToken accessToken = userRequest.getAccessToken();
Jwt jwt = jwtDecoder.decode(accessToken.getTokenValue());
Assert.notNull(userRequest, "userRequest cannot be null");
OAuth2AccessToken token = userRequest.getAccessToken();
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
// for (String authority : token.getScopes()) {
// authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
// }
Object authoritiesObject = jwt.getClaims().get("authorities");
if (authoritiesObject instanceof List<?>) {
List<String> authoritiesList = (List<String>) authoritiesObject;
for (String authority : authoritiesList) {
authorities.add(new SimpleGrantedAuthority(authority));
}
}
Map<String, Object> userAttributes = new LinkedHashMap<>();
userAttributes.put(userNameAttributeName, jwt.getClaims().get(userNameAttributeName));
userAttributes.put("sub", "user");
return new DefaultOAuth2User(authorities, userAttributes, "sub");
}
}
Override AuthorizationRequestRepository
to make it stateless and use cookie to store the serialized, encrypted OAuth2AuthorizationRequest
config.authorizationEndpoint(subconfig -> {
subconfig.authorizationRequestRepository(this.customStatelessAuthorizationRequestRepository);
});
@Component
public class CustomStatelessAuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
@Override
public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
return this.retrieveCookie(request);
}
@Override
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
if (authorizationRequest == null) {
this.removeCookie(response);
return;
}
this.attachCookie(response, authorizationRequest);
}
@Override
@Deprecated
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) {
return this.retrieveCookie(request);
}
//
// There are also some private helper methods in this class for:
// - attaching the cookie to the outgoing response
// - retrieving the cookie from an incoming request
//
// Plus a bit of functionality to encrypt the cookie payload,
// ensuring that the cookie doesn't get tampered with
//
}
oauth2Login.withObjectPostProcessor(postProcessor);
private static OAuth2AuthenticationToken createAuthenticationResult(OAuth2LoginAuthenticationToken authenticationResult) {
OAuth2AuthenticationToken oAuth2AuthenticationToken = new OAuth2AuthenticationToken(authenticationResult.getPrincipal(), authenticationResult.getAuthorities(),
authenticationResult.getClientRegistration().getRegistrationId());
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes servletRequestAttributes) {
HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();
httpServletRequest.setAttribute("access_token", authenticationResult.getAccessToken().getTokenValue());
if (authenticationResult.getRefreshToken() != null) {
httpServletRequest.setAttribute("refresh_token", authenticationResult.getRefreshToken().getTokenValue());
}
}
return oAuth2AuthenticationToken;
}
private static final Converter<OAuth2LoginAuthenticationToken, OAuth2AuthenticationToken> authenticationResultConverter = AuthConfig::createAuthenticationResult;
private static final ObjectPostProcessor<OAuth2LoginAuthenticationFilter> postProcessor = new ObjectPostProcessor<>() {
@Override
public <O extends OAuth2LoginAuthenticationFilter> O postProcess(O object) {
object.setAuthenticationResultConverter(authenticationResultConverter);
return object;
}
};
Pass to a controller after login that will set split token via javascript and also the cookie in the response.
oauth2Login.successHandler(successHandler());
@Bean
AuthenticationSuccessHandler successHandler() {
return new ForwardAuthenticationSuccessHandler("/index");
}
@RestController
public static class GreetController {
@GetMapping("/index")
public String index(HttpServletRequest httpServletRequest) {
return
"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>InfoArchive Redirect</title>
</head>
<body>
<h1>Access Token</h1>
<pre>
"""
+
httpServletRequest.getAttribute("access_token")
+
"""
<h1>Refresh Token</h1>
<pre>
"""
+
httpServletRequest.getAttribute("refresh_token")
+
"""
</pre>
<script>
window.addEventListener("load", () => {
setTimeout(() => {
window.location.replace("http://127.0.0.1:8082/");
}, 10000);
});
</script>
</body>
</html>
""";
}
DefaultAuthorizationCodeTokenResponseClient
handles the token response from the authorization server, which contains the access token and optionally a refresh token. The response is converted to an OAuth2AccessTokenResponse
and returned to the OAuth2LoginAuthenticationProvider
.
OAuth2LoginAuthenticationProvider
OAuth2LoginAuthenticationToken
is the result of the authentication process. It contains the OAuth2User
and the OAuth2AccessToken
that were obtained during the authorization code grant flow.