With one of the previous blog posts, we configured a Thymeleaf Spring Boot application for an OAuth 2 Login with Spring Security and AWS Cognito. While this article focussed on the setup and login mechanism, the logout functionality was only half-way implemented. Our end-users are still logged in at the identity provider. Let's adjust the application for an additional logout at AWS Cognito using Spring Security.
Spring Boot Project Setup
This blog post builds on top of Thymeleaf OAuth2 Login with Spring Security and AWS Cognito. Make sure to first read through the existing article to get a basic understanding of the application set up as we're not going to cover the required setup steps. This blog post focuses solely on the logout functionality.
As part of the GitHub repository, you'll find a sample CloudFormation stack to create and configure the AWS Cognito instance.
Why Do We Need To Configure the Logout?
With OpenID Connect and Spring Security, our users will have two sessions: one application-specific and one at the identity provider. Whenever a user signs out, Spring Security (by default) will invalidate the first session, and our users would still be logged in at the identity provider.
Every subsequent login attempt will automatically log them into our application without asking for their credentials (expect their identity provider's session expired). For some applications, this might be a feature. For other use cases, we might want to fully log out our users so that they can easily switch accounts.
The OIDC specification defines how a client can perform the logout at the identity provider: OpenID Connect RP-Initiated Logout 1.0 (currently in draft mode):
This specification complements the OpenID Connect Core 1.0 [OpenID.Core] specification by enabling the Relying Party to request that an End-User be logged out by the OpenID Provider.
The term Relying Party is just a different word for the OAuth 2.0 Client: the Spring Boot application in our example. With End-User, the specification refers to any user of our application that can log in. The OpenID provider for our setup is AWS Cognito.
Let's see how Spring Security can help us here.
Doesn't Spring Security Support the “Full Logout”?
Yes, it does.
But …
… only for identity providers that are following the OpenID Connect RP-Initiated Logout specification. As this spec is currently in draft mode, not every provider already implements it.
In case we're working with an identity provider that supports it (e.g. Keycloak), adding this logout mechanism requires a little adjustment for our WebSecurityConfig
. Spring Security provides a logout success handler for this scenario: OidcClientInitiatedLogoutSuccessHandler
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 WebSecurityConfig extends WebSecurityConfigurerAdapter { private final ClientRegistrationRepository clientRegistrationRepository; public WebSecurityConfig(ClientRegistrationRepository clientRegistrationRepository) { this.clientRegistrationRepository = clientRegistrationRepository; } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .and() .authorizeRequests(authorize -> authorize.mvcMatchers("/").permitAll() .anyRequest().authenticated()) .oauth2Login() .and() .logout() // works out-of-the-box for identity provider supporting the logout spec .logoutSuccessHandler(new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository)); } } |
Unfortunately, AWS Cognito doesn't implement the RP-Initiated Logout specification (yet). However, AWS Cognito offers a proprietary logout mechanism.
AWS Cognito Logout Mechanism
AWS Cognito defines a LOGOUT Endpoint that we can use to sign out the end-user. This endpoint expects two request parameters and only support HTTPS and GET
requests:
1 2 3 | GET https://rieckpil-test.auth.eu-central-1.amazoncognito.com/logout? client_id=ad398u21ijw30ow3939& logout_uri=http://localhost:8080 |
When using an AWS Cognito hosted signup page, the logout endpoint has the following structure https://<DOMAIN_PREFIX>.auth.<AWS_REGION>.amazoncognito.com/logout
.
Unfortunately, AWS Cognito doesn't expose this logout URL as part of the OAuth 2.0 discovery endpoint. Identity providers that are compatible with the RP-Initiated specification return a end_session_endpoint
. As we don't have this attribute available for AWS Cognito, we have to construct the URL on our own, e.g., passing all dynamic parts of the URL to our application or defining it as a CloudFormation output:
1 2 3 4 5 | Outputs: UserPoolLogoutUrl: Value: !Sub 'https://${LoginPageDomainPrefix}.auth.${AWS::Region}.amazoncognito.com/logout' Export: Name: UserPool::LogoutURL |
Let's take a look at the two parameters we pass to this endpoint.
The client_id
refers to the OAuth 2.0 client ID of our Spring Boot application. We already configure this ID as part of our application.yml
file, and hence our application can inject it.
Next, the logout_uri
defines a URL that AWS Cognito will redirect the user to after signing them out. We can choose any target that we want our users to land after logging out, e.g., a public welcome page of our application.
There's only one requirement for this redirect. The URL has to match one of the valid logout URLs for our AWS Cognito UserPoolClient
. Otherwise, the logout mechanism will fail:
1 2 3 4 5 6 7 8 9 10 11 12 | UserPoolClient: Type: AWS::Cognito::UserPoolClient Properties: ClientName: !Sub ${ApplicationName}-client GenerateSecret: true UserPoolId: !Ref UserPool CallbackURLs: - https://app.mycompany.org/login/oauth2/code/cognito - http://localhost:8080/login/oauth2/code/cognito LogoutURLs: - https://app.mycompany.org - http://localhost:8080 |
With this information about the AWS Cognito Logout endpoint, let's see how we can configure Spring Security to invoke it as part of the logout procedure.
Supporting AWS Cognito's Logout With Spring Security
Due to Spring Security's great extendability, we can define our own LogoutHandler
. Spring Security will invoke this handler as part of its own logout process (invalidating the HTTP session and clearing the SecurityContextHolder
).
By default, this logout mechanism is mapped to /logout
. We don't need an own controller mapping for this endpoint as Spring Security will handle any incoming HTTP requests to this endpoint.
As AWS Cognito expects an HTTPS request for the logout, we can extend Spring Security's SimpleUrlLogoutSUccessHandler
that is also used by the OidcClientInitiatedLogoutSuccessHandler
.
What's left is to override the determineTargetUrl
and create the URL that Spring Security will invoke as part of the logout process:
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 | public class CognitoOidcLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { private final String logoutUrl; private final String clientId; public CognitoOidcLogoutSuccessHandler(String logoutUrl, String clientId) { this.logoutUrl = logoutUrl; this.clientId = clientId; } @Override protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { UriComponents baseUrl = UriComponentsBuilder .fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) .replacePath(request.getContextPath()) .replaceQuery(null) .fragment(null) .build(); return UriComponentsBuilder .fromUri(URI.create(logoutUrl)) .queryParam("client_id", clientId) .queryParam("logout_uri", baseUrl) .encode(StandardCharsets.UTF_8) .build() .toUriString(); } } |
This basic implementation expects both the clientId
and logoutUrl
as parameters. We're redirecting the user to the base URL of our application. We could also make this configurable.
It's possible to determine both theclientId
and parts of thelogoutUrl
by injecting the ClientRegistrationRepository
and retrieving the OAuth 2.0 Metadata
. However, this includes String
manipulation on various attributes of the metadata and makes it a little bit more brittle.
The most intuitive approach is to inject both values with @Value
and configure them as part of our application.yml
or as an environment variable. The clientId
is already part of our Spring Security OAuth 2.0 setup, and hence only the logoutUrl
is left to configure:
1 2 3 4 5 6 7 8 9 10 | spring: security: oauth2: client: registration: cognito: clientId: ad398u21ijw30ow3939 # further configuration cognito: logoutUrl: https://rieckpil-test.auth.eu-central-1.amazoncognito.com/logout |
Configure The Logout With Our Custom Logout Success Handler
Putting it all together, we can add our CognitoOidcLogoutSuccessHandler
as the logoutSuccessHandler
when specifying the security setup for our application:
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 | @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private final String clientId; private final String logoutUrl; public WebSecurityConfig( @Value("${spring.security.oauth2.client.registration.cognito.clientId}") String clientId, @Value("${cognito.logoutUrl}") String logoutUrl) { this.clientId = clientId; this.logoutUrl = logoutUrl; } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .and() .authorizeRequests(authorize -> authorize.mvcMatchers("/").permitAll() .anyRequest().authenticated()) .oauth2Login() .and() .logout() .logoutSuccessHandler(new CognitoOidcLogoutSuccessHandler(logoutUrl, clientId)); } } |
With this logout handler in place, Spring Security ensures to call the AWS Cognito logout endpoint as part of its logout mechanism. Every subsequent login attempt will display the AWS Cognito login form to our users. They have to enter their credentials again and can now seamlessly switch accounts.
We don't need any further adjustments to our Thymeleaf view and can rely on the existing logout button:
1 2 3 4 5 | <div sec:authorize="isAuthenticated()"> <form method="post" th:action="@{/logout}"> <input type="submit" class="btn btn-danger" value="Logout"/> </form> </div> |
For more deep-dive on AWS Cognito, Spring Security, and Spring Boot, please take a look at the Stratospheric Book, which I'm co-authoring. For the book's sample application, we're using AWS Cognito for the login and registration functionality.
Furthermore, this book will teach you everything you need to know to get your Spring Boot application into production with AWS. Among other things, this includes:
- AWS CloudFormation introduction
- Deep dive into the AWS CDK (Cloud Development Kit)
- Build a continuous deployment pipeline
- Connect to various AWS Services (RDS, SES, SQS, etc.) with Spring Boot using Spring Cloud AWS
- … and much more
If you’re interested in learning about building applications with Spring Boot and AWS from top to bottom, make sure to check it out!
The source code for this OIDC Logout example with AWS Cognito and Spring Security is available on GitHub.
Have fun securing your application with AWS Cognito,
Philip