From 6f6f679c6534170e5fefef82a2670a22262c5bed Mon Sep 17 00:00:00 2001 From: Warren Parad Date: Sun, 17 Sep 2023 12:28:49 +0200 Subject: [PATCH] Force expiries to happen 10 seconds before tokens actually expiry. This creates a necessary buffer for expiring tokens and prevents an bad UX where users are lose their current work. --- src/index.js | 6 +++--- src/jwtManager.js | 30 +++++++++++++++++++++----- src/userIdentityTokenStorageManager.js | 1 + 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/index.js b/src/index.js index 4c7e989..4647865 100644 --- a/src/index.js +++ b/src/index.js @@ -198,7 +198,7 @@ class LoginClient { try { const tokenResult = await this.httpClient.post(`/authentication/${authRequest.nonce}/tokens`, this.enableCredentials, request); const idToken = jwtManager.decode(tokenResult.data.id_token); - const expiry = tokenResult.data.expires_in && new Date(Date.now() + tokenResult.data.expires_in * 1000) || new Date(idToken.exp * 1000); + const expiry = idToken.exp && new Date(idToken.exp * 1000) || tokenResult.data.expires_in && new Date(Date.now() + tokenResult.data.expires_in * 1000); document.cookie = cookieManager.serialize('authorization', tokenResult.data.access_token || '', { expires: expiry, path: '/', sameSite: 'strict' }); userIdentityTokenStorageManager.set(tokenResult.data.id_token, expiry); userSessionResolver(); @@ -227,7 +227,7 @@ class LoginClient { // * This prevents canonical replay attacks, and fall through. If the user is already logged in, then the new log in attempt is ignored. if (!authRequest.nonce || authRequest.nonce === urlSearchParams.get('nonce')) { const idToken = jwtManager.decode(urlSearchParams.get('id_token')); - const expiry = Number(urlSearchParams.get('expires_in')) && new Date(Date.now() + Number(urlSearchParams.get('expires_in')) * 1000) || new Date(idToken.exp * 1000); + const expiry = idToken.exp && new Date(idToken.exp * 1000) || Number(urlSearchParams.get('expires_in')) && new Date(Date.now() + Number(urlSearchParams.get('expires_in')) * 1000); document.cookie = cookieManager.serialize('authorization', urlSearchParams.get('access_token') || '', { expires: expiry, path: '/', sameSite: 'strict' }); userIdentityTokenStorageManager.set(urlSearchParams.get('id_token'), expiry); userSessionResolver(); @@ -251,7 +251,7 @@ class LoginClient { // In the case that the session contains non cookie based data, store it back to the cookie for this domain if (sessionResult.data.access_token) { const idToken = jwtManager.decode(sessionResult.data.id_token); - const expiry = sessionResult.data.expires_in && new Date(Date.now() + sessionResult.data.expires_in * 1000) || new Date(idToken.exp * 1000); + const expiry = idToken.exp && new Date(idToken.exp * 1000) || sessionResult.data.expires_in && new Date(Date.now() + sessionResult.data.expires_in * 1000); document.cookie = cookieManager.serialize('authorization', sessionResult.data.access_token || '', { expires: expiry, path: '/', sameSite: 'strict' }); userIdentityTokenStorageManager.set(sessionResult.data.id_token, expiry); } diff --git a/src/jwtManager.js b/src/jwtManager.js index aefec89..c1891e6 100644 --- a/src/jwtManager.js +++ b/src/jwtManager.js @@ -2,8 +2,19 @@ const base64url = require('./base64url'); class JwtManager { decode(token) { + if (!token) { + return null; + } + try { - return token && JSON.parse(base64url.decode(token.split('.')[1])); + const parsedToken = JSON.parse(base64url.decode(token.split('.')[1])); + // If the identity expires in less than 10 seconds from now, assume it is already expired. + // * This blocks issues with intermittent access, and subsequent issues when the token has a limited finite lifetime + // * All the Authress token server returns 5 second long JWT lifetimes to prevent issues with browsers refusing 0 second long lifetimes, so a buffer is required + if (parsedToken.exp) { + parsedToken.exp = parsedToken.exp - 10; + } + return parsedToken; } catch (error) { return null; } @@ -26,11 +37,20 @@ class JwtManager { } decodeFull(token) { + if (!token) { + return null; + } + try { - return token && { - header: JSON.parse(base64url.decode(token.split('.')[0])), - payload: JSON.parse(base64url.decode(token.split('.')[1])) - }; + const header = JSON.parse(base64url.decode(token.split('.')[0])); + const payload = JSON.parse(base64url.decode(token.split('.')[1])); + // If the identity expires in less than 10 seconds from now, assume it is already expired. + // * This blocks issues with intermittent access, and subsequent issues when the token has a limited finite lifetime + // * All the Authress token server returns 5 second long JWT lifetimes to prevent issues with browsers refusing 0 second long lifetimes, so a buffer is required + if (payload.exp) { + payload.exp = payload.exp - 10; + } + return { header, payload }; } catch (error) { return null; } diff --git a/src/userIdentityTokenStorageManager.js b/src/userIdentityTokenStorageManager.js index 2cc82ec..1c5513d 100644 --- a/src/userIdentityTokenStorageManager.js +++ b/src/userIdentityTokenStorageManager.js @@ -32,6 +32,7 @@ class UserIdentityTokenStorageManager { if (!idToken) { return this.getUserCookie(); } + if (expiry < Date.now()) { return null; }