1. Overview
In this tutorial, we’ll continue exploring the OAuth2 Authorization Code flow that we started putting together in our previous article and we’ll focus on how to handle the Refresh Token in an Angular app. We’ll also be making use of the Zuul proxy.
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: OAuth2 for a Spring REST API – Handle the Refresh Token in AngularJS (legacy OAuth stack)
2. Access Token Expiration
First, remember that the client was obtaining an Access Token using an Authorization Code grant type in two steps. In the first step, we obtain the Authorization Code. And in the second step, we actually obtain the Access Token.
Our Access Token is stored in a cookie which will expire based on when the Token itself expires:
var expireDate = new Date().getTime() + (1000 * token.expires_in);
Cookie.set("access_token", token.access_token, expireDate);
What’s important to understand is that the cookie itself is only used for storage and it doesn’t drive anything else in the OAuth2 flow. For example, the browser will never automatically send out the cookie to the server with requests, so we are secured here.
But note how we actually define this retrieveToken() function to get the Access Token:
retrieveToken(code) {
let params = new URLSearchParams();
params.append('grant_type','authorization_code');
params.append('client_id', this.clientId);
params.append('client_secret', 'newClientSecret');
params.append('redirect_uri', this.redirectUri);
params.append('code',code);
let headers =
new HttpHeaders({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8'});
this._http.post('http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token',
params.toString(), { headers: headers })
.subscribe(
data => this.saveToken(data),
err => alert('Invalid Credentials'));
}
We are sending the client secret in the params, which is not really a secure way to handle this. Let’s see how we can avoid doing this.
3. The Proxy
So, 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. All the sensitive information is going to be handled at this layer.
The front-end client will be now hosted as a Boot application so that we can connect seamlessly to our embedded Zuul proxy using the Spring Cloud Zuul starter.
If you want to go over the basics of Zuul, have a quick read of the main Zuul article.
Now let’s configure the routes of the proxy:
zuul:
routes:
auth/code:
path: /auth/code/**
sensitiveHeaders:
url: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/auth
auth/token:
path: /auth/token/**
sensitiveHeaders:
url: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token
auth/refresh:
path: /auth/refresh/**
sensitiveHeaders:
url: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token
auth/redirect:
path: /auth/redirect/**
sensitiveHeaders:
url: http://localhost:8089/
auth/resources:
path: /auth/resources/**
sensitiveHeaders:
url: http://localhost:8083/auth/resources/
We have set up routes to handle the following:
- auth/code – get the Authorization Code and save it in a cookie
- auth/redirect – handle the redirect to the Authorization Server’s login page
- auth/resources – map to the Authorization Server’s corresponding path for its login page resources (css and js)
- auth/token – get the Access Token, remove refresh_token from the payload and save it in a cookie
- auth/refresh – get the Refresh Token, remove it from the payload and save it in a cookie
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.
Next, let’s look at all these one by one.
4. Get the Code Using Zuul Pre Filter
The first use of the proxy is simple – we set up a request to get the Authorization Code:
@Component
public class CustomPreZuulFilter extends ZuulFilter {
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest req = ctx.getRequest();
String requestURI = req.getRequestURI();
if (requestURI.contains("auth/code")) {
Map<String, List> params = ctx.getRequestQueryParams();
if (params == null) {
params = Maps.newHashMap();
}
params.put("response_type", Lists.newArrayList(new String[] { "code" }));
params.put("scope", Lists.newArrayList(new String[] { "read" }));
params.put("client_id", Lists.newArrayList(new String[] { CLIENT_ID }));
params.put("redirect_uri", Lists.newArrayList(new String[] { REDIRECT_URL }));
ctx.setRequestQueryParams(params);
}
return null;
}
@Override
public boolean shouldFilter() {
boolean shouldfilter = false;
RequestContext ctx = RequestContext.getCurrentContext();
String URI = ctx.getRequest().getRequestURI();
if (URI.contains("auth/code") || URI.contains("auth/token") ||
URI.contains("auth/refresh")) {
shouldfilter = true;
}
return shouldfilter;
}
@Override
public int filterOrder() {
return 6;
}
@Override
public String filterType() {
return "pre";
}
}
We’re using a filter type of pre to process the request before passing it on.
In the filter’s run() method, we add query parameters for response_type, scope, client_id and redirect_uri – everything that our Authorization Server needs to take us to its login page and send back a Code.
Also note the shouldFilter() method. We are only filtering requests with the 3 URIs mentioned, others do not go through to the run method.
5. Put the Code in a Cookie Using Zuul Post Filter
What we’re planning to do here is to save the Code as a cookie so that we can send it across to the Authorization Server to get the Access Token. The Code is present as a query parameter in the request URL that the Authorization Server redirects us to after logging in.
We’ll set up a Zuul post-filter to extract this Code and set it in the cookie. This is not just a normal cookie, but a secured, HTTP-only cookie with a very limited path (/auth/token):
@Component
public class CustomPostZuulFilter extends ZuulFilter {
private ObjectMapper mapper = new ObjectMapper();
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
try {
Map<String, List> params = ctx.getRequestQueryParams();
if (requestURI.contains("auth/redirect")) {
Cookie cookie = new Cookie("code", params.get("code").get(0));
cookie.setHttpOnly(true);
cookie.setPath(ctx.getRequest().getContextPath() + "/auth/token");
ctx.getResponse().addCookie(cookie);
}
} catch (Exception e) {
logger.error("Error occured in zuul post filter", e);
}
return null;
}
@Override
public boolean shouldFilter() {
boolean shouldfilter = false;
RequestContext ctx = RequestContext.getCurrentContext();
String URI = ctx.getRequest().getRequestURI();
if (URI.contains("auth/redirect") || URI.contains("auth/token") || URI.contains("auth/refresh")) {
shouldfilter = true;
}
return shouldfilter;
}
@Override
public int filterOrder() {
return 10;
}
@Override
public String filterType() {
return "post";
}
}
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 Code from the Cookie
Now that we have the Code in the cookie, when the front-end Angular application tries to trigger a Token request, it’s going to send the request at /auth/token and so the browser, will of course, send that cookie.
So we’ll now have another condition in our pre filter in the proxy that will extract the Code from the cookie and send it along with other form parameters to obtain the Token:
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
...
else if (requestURI.contains("auth/token"))) {
try {
String code = extractCookie(req, "code");
String formParams = String.format(
"grant_type=%s&client_id=%s&client_secret=%s&redirect_uri=%s&code=%s",
"authorization_code", CLIENT_ID, CLIENT_SECRET, REDIRECT_URL, code);
byte[] bytes = formParams.getBytes("UTF-8");
ctx.setRequest(new CustomHttpServletRequest(req, bytes));
} catch (IOException e) {
e.printStackTrace();
}
}
...
}
private String extractCookie(HttpServletRequest req, String name) {
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (int i = 0; i < cookies.length; i++) {
if (cookies[i].getName().equalsIgnoreCase(name)) {
return cookies[i].getValue();
}
}
}
return null;
}
And here is our CustomHttpServletRequest – used to send our request body with the required form parameters converted to bytes:
public class CustomHttpServletRequest extends HttpServletRequestWrapper {
private byte[] bytes;
public CustomHttpServletRequest(HttpServletRequest request, byte[] bytes) {
super(request);
this.bytes = bytes;
}
@Override
public ServletInputStream getInputStream() throws IOException {
return new ServletInputStreamWrapper(bytes);
}
@Override
public int getContentLength() {
return bytes.length;
}
@Override
public long getContentLengthLong() {
return bytes.length;
}
@Override
public String getMethod() {
return "POST";
}
}
This will get us an Access Token from the Authorization Server in the response. Next, we’ll see how we are transforming the response.
7. 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.
We’ll add to our Zuul post-filter to extract the Refresh Token from the JSON body of the response and set it in the cookie. This is again a secured, HTTP-only cookie with a very limited path (/auth/refresh):
public Object run() {
...
else if (requestURI.contains("auth/token") || requestURI.contains("auth/refresh")) {
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.setPath(ctx.getRequest().getContextPath() + "/auth/refresh");
cookie.setMaxAge(2592000); // 30 days
ctx.getResponse().addCookie(cookie);
}
ctx.setResponseBody(responseBody);
}
...
}
As we can see, here we added a condition in our Zuul post-filter to read the response and extract the Refresh Token for the routes auth/token and auth/refresh. We are doing the exact same thing for the two because the Authorization Server essentially sends the same payload while obtaining the Access Token and the Refresh Token.
Then we removed refresh_token from the JSON response to make sure it’s never accessible to the front end outside of the cookie.
Another point to note here is that we set the max age of the cookie to 30 days – as this matches the expire time of the Token.
8. Get and Use the Refresh Token from the Cookie
Now that we have the Refresh Token in the cookie, when the front-end Angular application tries to trigger a token refresh, it’s going to send the request at /auth/refresh and so the browser, will, of course, send that cookie.
So we’ll now have another condition in our pre 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();
...
else if (requestURI.contains("auth/refresh"))) {
try {
String token = extractCookie(req, "token");
String formParams = String.format(
"grant_type=%s&client_id=%s&client_secret=%s&refresh_token=%s",
"refresh_token", CLIENT_ID, CLIENT_SECRET, token);
byte[] bytes = formParams.getBytes("UTF-8");
ctx.setRequest(new CustomHttpServletRequest(req, bytes));
} catch (IOException e) {
e.printStackTrace();
}
}
...
}
This is similar to what we did when we first obtained the Access Token. But notice that the form body is different. Now we’re sending a grant_type of refresh_token instead of authorization_code along with the token we’d saved before in the cookie.
After obtaining the response, it again goes through the same transformation in the pre filter as we saw earlier in section 7.
9. Refreshing the Access Token from Angular
Finally, let’s modify our simple front-end application and actually make use of refreshing the token:
Here is our function refreshAccessToken():
refreshAccessToken() {
let headers = new HttpHeaders({
'Content-type': 'application/x-www-form-urlencoded; charset=utf-8'});
this._http.post('auth/refresh', {}, {headers: headers })
.subscribe(
data => this.saveToken(data),
err => alert('Invalid Credentials')
);
}
Note how we’re simply using the existing saveToken() function – and just passing different inputs to it.
Also notice that we’re not adding any form parameters with the refresh_token ourselves – as that’s going to be taken care of by the Zuul filter.
10. Run the Front End
Since our front-end Angular client is now hosted as a Boot application, running it will be slightly different than before.
The first step is the same. We need to build the App:
mvn clean install
This will trigger the frontend-maven-plugin defined in our pom.xml to build the Angular code and copy the UI artifacts over to target/classes/static folder. This process overwrites anything else that we have in the src/main/resources directory. So we need to make sure and include any required resources from this folder, such as application.yml, in the copy process.
In the second step, we need to run our SpringBootApplication class UiApplication. Our client app will be up and running on port 8089 as specified in the application.yml.
11. Conclusion
In this OAuth2 tutorial we learned how to store the Refresh Token in an Angular client application, how to refresh an expired Access Token and how to leverage the Zuul proxy for all of that.
The full implementation of this tutorial can be found over on GitHub.