From 7444496985342e06bb15b0c393a5c0e574e01b7a Mon Sep 17 00:00:00 2001 From: Christoph Reiter <reiter.christoph@gmail.com> Date: Tue, 10 Nov 2020 16:55:36 +0100 Subject: [PATCH] auth-keycloak: make sure the token is always valid for at least 20 seconds The keycloak library currently only sends us an event in case the token is about to expire, which is problematic because there is a time window where we don't have a new token yet and on mobile the timers used might be suspended and come too late. To avoid this we check every 10 seconds that the token is valid for 30 and to work around suspended timers we also check on "visibilitychange" which should trigger then the website gets visible again after the browser sleeps. --- packages/auth/src/auth-keycloak.js | 1 + packages/auth/src/keycloak.js | 72 +++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/packages/auth/src/auth-keycloak.js b/packages/auth/src/auth-keycloak.js index 9717dbf8..45b395c0 100644 --- a/packages/auth/src/auth-keycloak.js +++ b/packages/auth/src/auth-keycloak.js @@ -229,6 +229,7 @@ export class AuthKeycloak extends LitElement { } disconnectedCallback() { + this._kcwrapper.close(); this._kcwrapper.removeEventListener('changed', this._onKCChanged); this._bus.close(); diff --git a/packages/auth/src/keycloak.js b/packages/auth/src/keycloak.js index f0e2a2e3..42db6858 100644 --- a/packages/auth/src/keycloak.js +++ b/packages/auth/src/keycloak.js @@ -55,6 +55,15 @@ const ensureURL = function(urlOrPath) { */ export class KeycloakWrapper extends EventTarget { + /* Minimum validity of the token in seconds */ + MIN_VALIDITY = 20; + + /* Interval at which the token validity is checked, in seconds */ + CHECK_INTERVAL = 10; + + /* Enables extra debug logging */ + DEBUG = false; + constructor(baseURL, realm, clientId, silentCheckSsoUri, idpHint) { super(); @@ -65,6 +74,24 @@ export class KeycloakWrapper extends EventTarget { this._initDone = false; this._silentCheckSsoUri = silentCheckSsoUri; this._idpHint = idpHint; + this._checkId = null; + + this._onVisibilityChanged = this._onVisibilityChanged.bind(this); + document.addEventListener("visibilitychange", this._onVisibilityChanged); + } + + /** + * This needs to be called or the instance will leak; + */ + close() { + document.removeEventListener("visibilitychange", this._onVisibilityChanged); + } + + _onVisibilityChanged() { + let isVisible = (document.visibilityState === 'visible'); + if (isVisible && this._keycloak.authenticated) { + this._checkTokeHasExpired(); + } } _onChanged() { @@ -97,6 +124,44 @@ export class KeycloakWrapper extends EventTarget { console.assert(refreshed, "token should have been refreshed"); } + async _checkTokeHasExpired() { + let refreshed; + + let minValidity = this.MIN_VALIDITY + this.CHECK_INTERVAL; + if (this.DEBUG) { + console.log(`Updating token if not valid for at least ${minValidity}s`); + } + try { + refreshed = await this._keycloak.updateToken(minValidity); + } catch (error) { + console.log('Failed to refresh the token', error); + } + + if (this.DEBUG && refreshed) + console.log("token has been refreshed"); + } + + async _onAuthSuccess() { + // We check every once in a while if the token is still valid and + // and refresh it if needed. + if (this._checkId !== null) { + clearInterval(this._checkId); + this._checkId = null; + } + this._checkId = setInterval(this._checkTokeHasExpired.bind(this), this.CHECK_INTERVAL * 1000); + + this._onChanged(); + } + + async _onAuthLogout() { + if (this._checkId !== null) { + clearInterval(this._checkId); + this._checkId = null; + } + + this._onChanged(); + } + async _ensureInstance() { if (this._keycloak !== null) return; @@ -112,8 +177,8 @@ export class KeycloakWrapper extends EventTarget { this._keycloak.onTokenExpired = this._onTokenExpired.bind(this); this._keycloak.onAuthRefreshSuccess = this._onChanged.bind(this); this._keycloak.onAuthRefreshError = this._onChanged.bind(this); - this._keycloak.onAuthLogout = this._onChanged.bind(this); - this._keycloak.onAuthSuccess = this._onChanged.bind(this); + this._keycloak.onAuthLogout = this._onAuthLogout.bind(this); + this._keycloak.onAuthSuccess = this._onAuthSuccess.bind(this); this._keycloak.onAuthError = this._onChanged.bind(this); this._keycloak.onReady = this._onReady.bind(this); } @@ -139,6 +204,9 @@ export class KeycloakWrapper extends EventTarget { pkceMethod: 'S256', }; + if (this.DEBUG) { + options['enableLogging'] = true; + } if (this._silentCheckSsoUri) { -- GitLab