If you're working on a Spring Security (and especially an OAuth) implementation, definitely have a look at the Learn Spring Security course:
>> LEARN SPRING SECURITYMocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.
Get started with mocking and improve your application tests using our Mockito guide:
Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.
Get started with understanding multi-threaded applications with our Java Concurrency guide:
Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:
Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.
But these can also be overused and fall into some common pitfalls.
To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:
Get started with Spring and Spring Boot, through the Learn Spring course:
>> LEARN SPRINGExplore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:
Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.
I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.
You can explore the course here:
Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.
Get started with Spring Data JPA through the guided reference course:
Refactor Java code safely β and automatically β with OpenRewrite.
Refactoring big codebases by hand is slow, risky, and easy to put off. Thatβs where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.
Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions β one for newcomers and one for experienced users. Youβll see how recipes work, how to apply them across projects, and how to modernize code with confidence.
Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.
1. Overview
In this quick tutorial, weβre going to show how we can add logout functionality to an OAuth Spring Security application.
Weβll see a couple of ways to do this. First, weβll see how to logout our Keycloak user from the OAuth application as described in Creating a REST API with OAuth2, and then, using the Zuul proxy we saw earlier.
Weβll use the OAuth stack in Spring Security 5. If you want to use the Spring Security OAuth legacy stack, have a look at this previous article: Logout in an OAuth Secured Application (using the legacy stack).
2. Logout Using Front-End Application
As the Access Tokens are managed by the Authorization Server, they will need to be invalidated at this level. The exact steps to do this will be slightly different depending on the Authorization Server youβre using.
In our example, as per the Keycloak documentation, for logging out directly from a browser application, we can redirect the browser to http://auth-server/auth/realms/{realm-name}/protocol/openid-connect/logout?redirect_uri=encodedRedirectUri.
Along with sending the redirect URI, we also need to pass an id_token_hint to Keycloakβs Logout endpoint. This should carry the encoded id_token value.
Letβs recall how weβd saved the access_token, weβll similarly save the id_token as well:
saveToken(token) {
var expireDate = new Date().getTime() + (1000 * token.expires_in);
Cookie.set("access_token", token.access_token, expireDate);
Cookie.set("id_token", token.id_token, expireDate);
this._router.navigate(['/']);
}
Importantly, in order to obtain the ID Token in the Authorization Serverβs response payload, we should include openid in the scope parameter.
Now letβs see the logging out process in action.
Weβll modify our function logout in App Service:
logout() {
let token = Cookie.get('id_token');
Cookie.delete('access_token');
Cookie.delete('id_token');
let logoutURL = "http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/logout?
id_token_hint=" + token + "&post_logout_redirect_uri=" + this.redirectUri;
window.location.href = logoutURL;
}
Apart from the redirection, we also need to discard the Access and ID Tokens that weβd obtained from the Authorization Server.
Hence, in the above code, first we deleted the tokens, and then redirected the browser to Keycloakβs logout API.
Notably, we passed in the redirect URI as http://localhost:8089/ β the one weβre using throughout the application β so weβll end up on the landing page after logging out.
The deletion of Access, ID and Refresh Tokens corresponding to the current session is performed at the Authorization Serverβs end. Our browser application had not saved the Refresh Token at all in this case.
3. Logout Using Zuul Proxy
In a previous article on Handling the Refresh Token, we have set up our application to be able to refresh the Access Token, using a Refresh Token. This implementation makes use of a Zuul proxy with custom filters.
Here weβll see how to add the logout functionality to the above.
This time around, weβll utilize another Keycloak API to log out a user. Weβll be invoking POST on the logout endpoint to log out a session via a non-browser invocation, instead of the URL redirect we used in the previous section.
3.1. Define Route for Logout
To start with, letβs add another route to the proxy in our application.yml:
zuul:
routes:
//...
auth/refresh/revoke:
path: /auth/refresh/revoke/**
sensitiveHeaders:
url: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/logout
//auth/refresh route
In fact, we added a sub-route to the already existing auth/refresh. Itβs important that we add the sub-route before the main route, otherwise, Zuul will always map the URL of the main route.
We added a sub route instead of a main one in order to have access to the HTTP-only refreshToken cookie, which was set to have a very limited path as /auth/refresh (and its sub-paths). Weβll see why we need the cookie in the next section.
3.2. POST to Authorization Serverβs /logout
Now letβs enhance the CustomPreZuulFilter implementation to intercept the /auth/refresh/revoke URL and add the necessary information to be passed on to the Authorization Server.
The form parameters required for logout are similar to those of the Refresh Token request, except there is no grant_type:
@Component
public class CustomPostZuulFilter extends ZuulFilter {
//...
@Override
public Object run() {
//...
if (requestURI.contains("auth/refresh/revoke")) {
String cookieValue = extractCookie(req, "refreshToken");
String formParams = String.format("client_id=%s&client_secret=%s&refresh_token=%s",
CLIENT_ID, CLIENT_SECRET, cookieValue);
bytes = formParams.getBytes("UTF-8");
}
//...
}
}
Here, we simply extracted the refreshToken cookie and sent in the required formParams.
3.3. Remove the Refresh Token
When revoking the Access Token using the logout redirection as we saw earlier, the Refresh Token associated with it is also invalidated by the Authorization Server.
However, in this case, the httpOnly cookie will remain set on the Client. Given that we canβt remove it via JavaScript, we need to remove it from the server-side.
For that, letβs add to the CustomPostZuulFilter implementation that intercepts the /auth/refresh/revoke URL so that it will remove the refreshToken cookie when encountering this URL:
@Component
public class CustomPostZuulFilter extends ZuulFilter {
//...
@Override
public Object run() {
//...
String requestMethod = ctx.getRequest().getMethod();
if (requestURI.contains("auth/refresh/revoke")) {
Cookie cookie = new Cookie("refreshToken", "");
cookie.setMaxAge(0);
ctx.getResponse().addCookie(cookie);
}
//...
}
}
3.4. Remove the Access Token from the Angular Client
Besides revoking the Refresh Token, the access_token cookie will also need to be removed from the client-side.
Letβs add a method to our Angular controller that clears the access_token cookie and calls the /auth/refresh/revoke POST mapping:
logout() {
let headers = new HttpHeaders({
'Content-type': 'application/x-www-form-urlencoded; charset=utf-8'});
this._http.post('auth/refresh/revoke', {}, { headers: headers })
.subscribe(
data => {
Cookie.delete('access_token');
window.location.href = 'http://localhost:8089/';
},
err => alert('Could not logout')
);
}
This function will be called when clicking on the Logout button:
<a class="btn btn-default pull-right"(click)="logout()" href="#">Logout</a>
4. Conclusion
In this quick, but in-depth tutorial, weβve shown how we can logout a user from an OAuth secured application and invalidate the tokens of that user.
