When using the implicit authentication flow refresh tokens cannot be requested or used, since the client application cannot be explicitly or securely authenticated and therefore cannot be trusted with such a sensitive token. This also applies to any flow on a public client incapable of keeping a secret or making secure back channel requests. If a refresh token intended for a such a client was stolen, the thief could use it to request access tokens for that user, without their knowledge or consent.
When using a client application running in the browser, which the OpenID Connect implicit flow was designed for, we expect the user to be present at the client application. They might be currently in a different tab or even on a different application than the browser, but the session is still active. This means that if their access token expires, they should still be around to authorize another to be issued. We’re not expecting the client application to be performing any sort of background tasks or long-running processing.
But what if, for instance, the user was filling out a form in the application and their access token expired? Maybe it’s some gargantuan form that takes 3 hours to complete, or maybe they simply got up to get a cup of tea. Default behaviour would say that whatever resource you’re calling would return a 401 Unauthorized, causing whatever OpenID Connect middleware you’re using to cart you off to your OpenID Provider to get new tokens, and then returning you to the previous page you were on. This would also mean an empty form, with all the data lost.
So, you could create a mechanism that stores this data before you redirect the user to the OpenID Provider, but wouldn’t a pre-emptive approach be better? As I said, refresh tokens aren’t an option here, but there is another mechanism we can use, commonly called “silent refresh”.
OAuth recommendations for browser-based client applications have since evolved from the implicit flow to authorization code flow with PKCE. The following silent refresh approach is still valid for the updated recommendations and even for backend-for-frontend implementations.
Silent Refresh
Silent refresh uses the assumption that the user is still logged into the OpenID Provider to automatically make another OpenID Connect authorization request and receive new tokens. This is done behind the scenes without interrupting the user experience.
This request is typically made in an iFrame, too small for the user to see, with the request looking very similar to the authorization request that the client application made to initially authenticate the user, albeit with a few tweaks.
Most notably, we’re going to be making use of the prompt parameter available to us in an OpenID Connect authorization request.
This parameter is used to give a hint to your OpenID Provider as to how the request should be handled.
By using the value none
we tell our provider that it must not display any authentication or consent pages.
If the user is still authenticated with the OpenID Provider, then this is fine, and the provider will simply response successfully with new tokens, just like normal.
However, if the user is not authenticated or needs to interact with the provider in some way, then the provider will return an error.
So, the basic flow of silent refresh looks like this:
- Silent refresh triggered (e.g. by event triggered by access token lifetime, or 401 received from protected resource)
- Open iFrame
- Issue OpenID Connect authorization request including a `prompt` parameter with a value of `none`
-
Receive response
- If tokens received, update session
- Else, interrupt UX and redirect main window to OpenID Provider
Implementing Silent Refresh using Angular CLI and oidc-client
Here we are going to build upon the Angular application from my previous tutorial, again using the oidc-client-js library to add OpenID Connect support. Other OpenID Connect libraries are available for Angular or TypeScript, but oidc-client is plain JavaScript and can be used with any JS framework. Therefore, if you’re not using Angular, the below code will still be of some use.
The first thing we need to do is to update out UserManagerSettings
found in our auth service:
export function getClientSettings(): UserManagerSettings {
return {
// existing settings
automaticSilentRenew: true,
silent_redirect_uri: 'http://localhost:4200/silent-refresh.html'
};
}
Here we have enabled silent refresh using the automaticSilentRenew
property.
This will cause an event to be fired close to when our access token is about to expire, that triggers a silent refresh request in an iFrame on the user’s current page.
Our silent refresh request can also use a different redirect URI. This allows us to have different callback logic based on whether we are dealing with user interaction or silent refresh. You are welcome to use your existing redirect endpoint.
In this case I am simply redirecting to a static html page. We could have an Angular controlld page be loaded, but I’m averse to loading up another instance of my Angular app in an iFrame. By using this method, we keep things lightweight and performant.
Don’t forget to add this route as an authorized redirect URI within your OpenID Provider.
silent-refresh.html
<head>
<title></title>
</head>
<body>
<script src="oidc-client.min.js"></script>
<script>
new UserManager().signinSilentCallback()
.catch((err) => {
console.log(err);
});
</script>
</body>
Here we are simply loading a very basic page, that simply calls the UserManager
s signinSilentCallback
method.
Depending where you are getting oidc-client.min.js from (dist
or lib
), you may need to change UserManager
to Oidc.UserManager
.
And then, to include this file in our build, we simply update our .angular-cli.json
with the following entry:
{
"apps": [{
"assets": [
"silent-refresh.html",
"oidc-client.min.js"
]
}]
}
I’ve included a static copy of oidc-client.js so that I can easily access the file outside of Angular. How you route this is completely up to, I’m just lazy.
And that’s all there is to it. The next time your access token is about to expire, in your network traffic you’ll see an authorization request, followed by the silent-refresh page loading.
If you want to see this in action and prove it working, just set access token lifetime to 60 seconds and watch you network traffic go crazy.
Handling Errors
You can still handle errors by listening out for the silentRenewError event that is raised when an automatic silent renew fails either by timing out or after receiving an error.
You can subscribe to this event using:
this.manager. events.addSilentRenewError(function(){
// custom logic here
});