SpringBoot security sixth look: OAuth2 Server detail
Introduction
Long story short, for this post, I want to finish the detail rundown of the spring-boot-starter-oauth2-authorization-server package.
Overview
This is what we know from my previous post. OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
in OAuth2AuthorizationServerWebSecurityConfiguration.java will configure following filters:
OAuth2AuthorizationServerConfigurer does:
- OAuth2ClientAuthenticationConfigurer adds OAuth2ClientAuthenticationFilter for
- “/oauth2/token”, “/oauth2/revoke”, “/oauth2/introspect”, and “/oauth2/device_authorization”.
- OAuth2AuthorizationServerMetadataEndpointConfigurer adds OAuth2AuthorizationServerMetadataEndpointFilter for
- “/.well-known/oauth-authorization-server”.
- OAuth2AuthorizationEndpointConfigurer adds OAuth2AuthorizationEndpointFilter for
- “/oauth2/authorize”.
- OAuth2TokenEndpointConfigurer adds OAuth2TokenEndpointFilter for
- “/oauth2/token”.
- OAuth2TokenIntrospectionEndpointConfigurer adds OAuth2TokenIntrospectionEndpointFilter for
- “/oauth2/introspect”.
- OAuth2TokenRevocationEndpointConfigurer adds OAuth2TokenRevocationEndpointFilter for
- “/oauth2/revoke”.
- OAuth2DeviceAuthorizationEndpointConfigurer adds OAuth2DeviceAuthorizationEndpointFilter for
- “/oauth2/device_authorization”.
- OAuth2DeviceVerificationEndpointConfigurer adds OAuth2DeviceVerificationEndpointFilter for
- “/oauth2/device_verification”.
OidcConfigurer does:
- OidcProviderConfigurationEndpointConfigurer adds OidcProviderConfigurationEndpointFilter for
- “/.well-known/openid-configuration”
- OidcLogoutEndpointConfigurer adds OidcLogoutEndpointFilter for
- “/connect/logout”
- OidcUserInfoEndpointConfigurer adds OidcUserInfoEndpointFilter for
- “/userinfo”
Debug
Starting up the server in debug mode, set the breakpoint in FilterChainProxy.java to see what requests are being processed.
Client startup
During client startup, client will send a request to “/.well-known/openid-configuration” asking about information like authorization endpoint, token endpoint, userinfo endpoint etc. As seen above, this is OidcProviderConfigurationEndpointFilter will handle this request. Look at the class internal filter:
1 |
|
The code is very straight forward, it simply builds a json response (MediaType.APPLICATION_JSON) that contains information about the authorization server. Also, we see that once the http response is created and committed, the method is returned. That means that rest of filters in the FilterChainProxy are ignored. (There is no filterChain.doFilter(request, response);
at the end).
Client access resource (1st time)
Now, let say if client wants to access “localhost:8080”, based on previous post, the AuthorizationFilter sends a request to “/oauth2/authorize”, which is handled by the OAuth2AuthorizationEndpointFilter. Obviously at this moment, the authorization is going to fail because the client have not logged in yet.
The interesting thing here is that, the code also checks if redirect_uri value in HttpRequest matches the value in RegisteredClient class. This happens at this.authenticationValidator.accept(authenticationContext);
in L121 of OAuth2AuthorizationCodeRequestAuthenticationProvider.java.
No Authorization, redirect to /login
Moving on, since no authorization is granted in OAuth2AuthorizationEndpointFilter, the filter chain continues and finally reaches AuthorizationFilter. The AuthorizationFilter will grant access if a valid AuthenticationToken exists. If not, an AccessDeniedException is thrown and caught by the ExceptionTranslationFilter that redirect requests to “/login”. Code detail described in fourth look post.
Handle /login, redirect to /oauth2/authorize
How UsernamePasswordAuthenticationFilter handles login is discussed at second look post
If login succeed (both username and password match), the successfulAuthentication(request, response, chain, authenticationResult);
method will call the successHandler to redirect the request to “/oauth2/authorize”. An example redirect URL looks like
We see from the URL that there are “response_type”, “client_id”, “scope”, “state”, “redirect_uri”, “nonce” and “continue” parameters.
Handle /oauth2/authorize
This time when OAuth2AuthorizationEndpointFilter check there is authorization granted (UsernamePasswordAuthenticationToken) and, by OAuth2AuthorizationCodeRequestAuthenticationProvider, converted to into an OAuth2AuthorizationConsentAuthenticationToken or OAuth2AuthorizationCodeRequestAuthenticationToken depending on if spring.security.oauth2.authorizationserver.client.login-client.require-authorization-consent
if set to true or not.
If the token requires user consent, the in sendAuthorizationConsent(...)
method in OAuth2AuthorizationEndpointFilter will call DefaultConsentPage.displayConsent(...)
to write the Consent Page’s html directly to HttpResponses.
1 | static void displayConsent(HttpServletRequest request, HttpServletResponse response, String clientId, |
Handle /oauth2/authorize, after consent
After consent page, if consent is confirmed, another request is sent to “/oauth2/authorize”. This time an OAuth2AuthorizationCodeRequestAuthenticationToken is created and a response is returned to the client. An example response url looks like
We see from the URL that there are “code” and “state”, “scope” parameters.
TODO: /token, /revoke, /introspect
It seems that when you hit the “logout” button. The client does not send “/revoke” to our authorization server. My personal guess is that /token, /revoke are used with third party oidc applications like GCP, Github, Okta etc.