Implementing a silent token renew in Angular for the OpenID Connect Implicit flow

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:

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/

Advertisements

3 comments

  1. […] Implementing a silent token renew in Angular for the OpenID Connect Implicit flow (Damien Bowden) […]

  2. […] Implementing a silent token renew in Angular for the OpenID Connect Implicit flow by Damien Bowden. […]

  3. Andy Smith · · Reply

    Hi, thanks for this. This must have been a lot of work. I tried getting IdentityServerWithIdentitySQLite, ResourceServer, and AngularClient all working. Everything works fine until, after successfully authenticating, AngularClient tries to call ResourceServer for the ‘dataevenrecords’ page. I get this error:

    “No ‘Access-Control-Allow-Origin’ header is present on the requested resource”

    Googling leads me to believe it’s a CORS config issue, but I notice that AngularClient has this in its Startup.cs:

    services.AddCors(options => {
    options.AddPolicy(“AllowAllOrigins”,
    builder => {
    builder
    .AllowAnyOrigin()
    .AllowAnyHeader()
    .AllowAnyMethod();
    });
    });

    … and the ResourceServer has this in its Startup.cs:

    var policy = new Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy();
    policy.Headers.Add(“*”);
    policy.Methods.Add(“*”);
    policy.Origins.Add(“*”);
    policy.SupportsCredentials = true;

    services.AddCors(x => x.AddPolicy(“corsGlobalPolicy”, policy));

    So it seems like there shouldn’t be any CORS issues. Is this caused by something else? Any help is much appreciated.

    Thanks,
    Andy

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: