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 tutorial, weβll continue exploring the OAuth password flow that we started putting together in more our previous article and weβll focus on how to handle the Refresh Token in an AngularJS app.
Note: this article is using the Spring OAuth legacy project. For the version of this article using the new Spring Security 5 stack, have a look at our article OAuth2 for a Spring REST API β Handle the Refresh Token in Angular.
2. Access Token Expiration
First, remember that the client was obtaining an Access Token when the user was logging into the application:
function obtainAccessToken(params) {
var req = {
method: 'POST',
url: "oauth/token",
headers: {"Content-type": "application/x-www-form-urlencoded; charset=utf-8"},
data: $httpParamSerializer(params)
}
$http(req).then(
function(data) {
$http.defaults.headers.common.Authorization= 'Bearer ' + data.data.access_token;
var expireDate = new Date (new Date().getTime() + (1000 * data.data.expires_in));
$cookies.put("access_token", data.data.access_token, {'expires': expireDate});
window.location.href="index";
},function() {
console.log("error");
window.location.href = "login";
});
}
Note how our Access Token is stored in a cookie which will expire based on when the token itself expires.
Whatβs important to understand is that the cookie itself is only used for storage and it doesnβt drive anything else in the OAuth flow. For example, the browser will never automatically send out the cookie to the server with requests.
Also note how we actually call this obtainAccessToken() function:
$scope.loginData = {
grant_type:"password",
username: "",
password: "",
client_id: "fooClientIdPassword"
};
$scope.login = function() {
obtainAccessToken($scope.loginData);
}
3. The Proxy
Weβre now going to have a Zuul proxy running in the front-end application and basically sitting between the front-end client and the Authorization Server.
Letβs configure the routes of the proxy:
zuul:
routes:
oauth:
path: /oauth/**
url: http://localhost:8081/spring-security-oauth-server/oauth
Whatβs interesting here is that weβre only proxying traffic to the Authorization Server and not anything else. We only really need the proxy to come in when the client is obtaining new tokens.
If you want to go over the basics of Zuul, have a quick read of the main Zuul article.
4. A Zuul Filter That Does Basic Authentication
The first use of the proxy is simple β instead of revealing our app βclient secretβ in javascript, we will use a Zuul pre-filter to add an Authorization header to access token requests:
@Component
public class CustomPreZuulFilter extends ZuulFilter {
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.getRequest().getRequestURI().contains("oauth/token")) {
byte[] encoded;
try {
encoded = Base64.encode("fooClientIdPassword:secret".getBytes("UTF-8"));
ctx.addZuulRequestHeader("Authorization", "Basic " + new String(encoded));
} catch (UnsupportedEncodingException e) {
logger.error("Error occured in pre filter", e);
}
}
return null;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public int filterOrder() {
return -2;
}
@Override
public String filterType() {
return "pre";
}
}
Now keep in mind that this doesnβt add any extra security and the only reason weβre doing it is because the token endpoint is secured with Basic Authentication using client credentials.
From the point of view of the implementation, the type of filter is especially worth noticing. Weβre using a filter type of βpreβ to process the request before passing it on.
5. Put the Refresh Token in a Cookie
On to the fun stuff.
What weβre planning to do here is to have the client get the Refresh Token as a cookie. Not just a normal cookie, but a secured, HTTP-only cookie with a very limited path (/oauth/token).
Weβll set up a Zuul post-filter to extract Refresh Token from the JSON body of the response and set it in the cookie:
@Component
public class CustomPostZuulFilter extends ZuulFilter {
private ObjectMapper mapper = new ObjectMapper();
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
try {
InputStream is = ctx.getResponseDataStream();
String responseBody = IOUtils.toString(is, "UTF-8");
if (responseBody.contains("refresh_token")) {
Map<String, Object> responseMap = mapper.readValue(
responseBody, new TypeReference<Map<String, Object>>() {});
String refreshToken = responseMap.get("refresh_token").toString();
responseMap.remove("refresh_token");
responseBody = mapper.writeValueAsString(responseMap);
Cookie cookie = new Cookie("refreshToken", refreshToken);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath(ctx.getRequest().getContextPath() + "/oauth/token");
cookie.setMaxAge(2592000); // 30 days
ctx.getResponse().addCookie(cookie);
}
ctx.setResponseBody(responseBody);
} catch (IOException e) {
logger.error("Error occured in zuul post filter", e);
}
return null;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public int filterOrder() {
return 10;
}
@Override
public String filterType() {
return "post";
}
}
A few interesting things to understand here:
- We used a Zuul post-filter to read response and extract refresh token
- We removed the value of the refresh_token from JSON response to make sure itβs never accessible to the front end outside of the cookie
- We set the max-age of the cookie to 30 days β as this matches the expire time of the token
In order to add an extra layer of protection against CSRF attacks, weβll add a Same-Site cookie header to all our cookies.
For that, weβll create a configuration class:
@Configuration
public class SameSiteConfig implements WebMvcConfigurer {
@Bean
public TomcatContextCustomizer sameSiteCookiesConfig() {
return context -> {
final Rfc6265CookieProcessor cookieProcessor = new Rfc6265CookieProcessor();
cookieProcessor.setSameSiteCookies(SameSiteCookies.STRICT.getValue());
context.setCookieProcessor(cookieProcessor);
};
}
}
Here weβre setting the attribute to strict, so that any cross site transfer of cookies is strictly withheld.
6. Get and Use the Refresh Token from the Cookie
Now that we have the Refresh Token in the cookie, when the front-end AngularJS application tries to trigger a token refresh, itβs going to send the request at /oauth/token and so the browser, will, of course, send that cookie.
So weβll now have another filter in the proxy that will extract the Refresh Token from the cookie and send it forward as a HTTP parameter β so that the request is valid:
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
...
HttpServletRequest req = ctx.getRequest();
String refreshToken = extractRefreshToken(req);
if (refreshToken != null) {
Map<String, String[]> param = new HashMap<String, String[]>();
param.put("refresh_token", new String[] { refreshToken });
param.put("grant_type", new String[] { "refresh_token" });
ctx.setRequest(new CustomHttpServletRequest(req, param));
}
...
}
private String extractRefreshToken(HttpServletRequest req) {
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (int i = 0; i < cookies.length; i++) {
if (cookies[i].getName().equalsIgnoreCase("refreshToken")) {
return cookies[i].getValue();
}
}
}
return null;
}
And here is our CustomHttpServletRequest β used to inject our refresh token parameters:
public class CustomHttpServletRequest extends HttpServletRequestWrapper {
private Map<String, String[]> additionalParams;
private HttpServletRequest request;
public CustomHttpServletRequest(
HttpServletRequest request, Map<String, String[]> additionalParams) {
super(request);
this.request = request;
this.additionalParams = additionalParams;
}
@Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> map = request.getParameterMap();
Map<String, String[]> param = new HashMap<String, String[]>();
param.putAll(map);
param.putAll(additionalParams);
return param;
}
}
Again, a lot of important implementation notes here:
- The Proxy is extracting the Refresh Token from the Cookie
- Itβs then setting it into the refresh_token parameter
- Itβs also setting the grant_type to refresh_token
- If there is no refreshToken cookie (either expired or first login) β then the Access Token request will be redirected with no change
7. Refreshing the Access Token from AngularJS
Finally, letβs modify our simple front-end application and actually make use of refreshing the token:
Here is our function refreshAccessToken():
$scope.refreshAccessToken = function() {
obtainAccessToken($scope.refreshData);
}
And here our $scope.refreshData:
$scope.refreshData = {grant_type:"refresh_token"};
Note how weβre simply using the existing obtainAccessToken function β and just passing different inputs to it.
Also notice that weβre not adding the refresh_token ourselves β as thatβs going to be taken care of by the Zuul filter.
8. Conclusion
In this OAuth tutorial, we learned how to store the Refresh Token in an AngularJS client application, how to refresh an expired Access Token, and how to leverage the Zuul proxy for all of that.
