This article shows how to implement a silent token renew in Angular using IdentityServer4 as the security token service server. The SPA Angular client implements the OpenID Connect Implicit Flow ‘id_token token’. When the id_token expires, the client requests new tokens from the server, so that the user does not need to authorise again.
Code: https://github.com/damienbod/AspNet5IdentityServerAngularImplicitFlow
Other posts in this series:
- OAuth2 Implicit Flow with Angular and ASP.NET Core 1.1 IdentityServer4
- Authorization Policies and Data Protection with IdentityServer4 in ASP.NET Core
- AngularJS OpenID Connect Implicit Flow with IdentityServer4
- Angular OpenID Connect Implicit Flow with IdentityServer4
- Secure file download using IdentityServer4, Angular and ASP.NET Core
- Angular secure file download without using an access token in URL or cookies
- Full Server logout with IdentityServer4 and OpenID Connect Implicit Flow
- IdentityServer4, Web API and Angular in a single project
- Extending Identity in IdentityServer4 to manage users in ASP.NET Core
- Implementing a silent token renew in Angular for the OpenID Connect Implicit flow
- OpenID Connect Session Management using an Angular application and IdentityServer4
When a user of the client app authorises for the first time, after a successful login on the STS server, the AuthorizedCallback function is called in the Angular application. If the server response and the tokens are successfully validated, as defined in the OpenID Connect specification, the silent renew is initialized, and the token validation method is called.
public AuthorizedCallback() { ... if (authResponseIsValid) { ... if (this._configuration.silent_renew) { this._oidcSecuritySilentRenew.initRenew(); } this.runTokenValidatation(); this._router.navigate([this._configuration.startupRoute]); } else { this.ResetAuthorizationData(); this._router.navigate(['/Unauthorized']); } }
The OidcSecuritySilentRenew Typescript class implements the iframe which is used for the silent token renew. This iframe is added to the parent HTML page. The renew is implemented in an iframe, because we do not want the Angular application to refresh, otherwise for example we would lose form data.
... @Injectable() export class OidcSecuritySilentRenew { private expiresIn: number; private authorizationTime: number; private renewInSeconds = 30; private _sessionIframe: any; constructor(private _configuration: AuthConfiguration) { } public initRenew() { this._sessionIframe = window.document.createElement('iframe'); console.log(this._sessionIframe); this._sessionIframe.style.display = 'none'; window.document.body.appendChild(this._sessionIframe); } ... }
The runTokenValidatation function starts an Observable timer. The application subscribes to the Observable which executes every 3 seconds. The id_token is validated, or more precise, checks that the id_token has not expired. If the token has expired and the silent_renew configuration has been activated, the RefreshSession function will be called, to get new tokens.
private runTokenValidatation() { let source = Observable.timer(3000, 3000) .timeInterval() .pluck('interval') .take(10000); let subscription = source.subscribe(() => { if (this._isAuthorized) { if (this.oidcSecurityValidation.IsTokenExpired(this.retrieve('authorizationDataIdToken'))) { console.log('IsAuthorized: isTokenExpired'); if (this._configuration.silent_renew) { this.RefreshSession(); } else { this.ResetAuthorizationData(); } } } }, function (err: any) { console.log('Error: ' + err); }, function () { console.log('Completed'); }); }
The RefreshSession function creates the required nonce and state which is used for the OpenID Implicit Flow validation and starts an authentication and authorization of the client application and the user.
public RefreshSession() { console.log('BEGIN refresh session Authorize'); let nonce = 'N' + Math.random() + '' + Date.now(); let state = Date.now() + '' + Math.random(); this.store('authStateControl', state); this.store('authNonce', nonce); console.log('RefreshSession created. adding myautostate: ' + this.retrieve('authStateControl')); let url = this.createAuthorizeUrl(nonce, state); this._oidcSecuritySilentRenew.startRenew(url); }
The startRenew sets the iframe src to the url for the OpenID Connect flow. If successful, the id_token and the access_token are returned and the application runs without any interupt.
public startRenew(url: string) { this._sessionIframe.src = url; return new Promise((resolve) => { this._sessionIframe.onload = () => { resolve(); } }); }
IdentityServer4 Implicit Flow configuration
The STS server, using IdentityServer4 implements the server side of the OpenID Implicit flow. The AccessTokenLifetime and the IdentityTokenLifetime properties are set to 30s and 10s. After 10s the id_token will expire and the client application will request new tokens. The access_token is valid for 30s, so that any client API requests will not fail. If you set these values to the same value, then the client will have to request new tokens before the id_token expires.
new Client { ClientName = "angularclient", ClientId = "angularclient", AccessTokenType = AccessTokenType.Reference, AccessTokenLifetime = 30, IdentityTokenLifetime = 10, AllowedGrantTypes = GrantTypes.Implicit, AllowAccessTokensViaBrowser = true, RedirectUris = new List<string> { "https://localhost:44311" }, PostLogoutRedirectUris = new List<string> { "https://localhost:44311/Unauthorized" }, AllowedCorsOrigins = new List<string> { "https://localhost:44311", "http://localhost:44311" }, AllowedScopes = new List<string> { "openid", "dataEventRecords", "dataeventrecordsscope", "securedFiles", "securedfilesscope", "role" } }
When the application is run, the user can login, and the tokens are refreshed every ten seconds as configured on the server.
Links:
http://openid.net/specs/openid-connect-implicit-1_0.html
https://github.com/IdentityServer/IdentityServer4
https://identityserver4.readthedocs.io/en/release/
