From a420319a8b422bfe8974d73e34fe6074f275aea0 Mon Sep 17 00:00:00 2001 From: Bastien Abadie Date: Mon, 20 Apr 2020 13:50:27 +0200 Subject: [PATCH] Handle expiry from Taskcluster OAuth (#168) --- src/components/auth/AuthController.js | 26 +++++++++---------- src/components/auth/UserSession.js | 37 +++++++++++++++++++++------ src/components/auth/oauth2.js | 27 +++---------------- src/config.js | 4 ++- src/views/ProfileMenu/index.jsx | 4 +++ 5 files changed, 51 insertions(+), 47 deletions(-) diff --git a/src/components/auth/AuthController.js b/src/components/auth/AuthController.js index c9bd451..9eeceb6 100644 --- a/src/components/auth/AuthController.js +++ b/src/components/auth/AuthController.js @@ -1,7 +1,9 @@ import mitt from 'mitt'; import UserSession from './UserSession'; -import { userSessionFromCode, renew as auth0Renew } from './oauth2'; +import { userSessionFromCode } from './oauth2'; + +const STORAGE_KEY = 'taskcluster_user_session'; /** * Controller for authentication-related pieces of the site. @@ -21,7 +23,7 @@ export default class AuthController { this.renewalTimer = null; window.addEventListener('storage', ({ storageArea, key }) => { - if (storageArea === localStorage && key === 'userSession') { + if (storageArea === localStorage && key === STORAGE_KEY) { this.loadUserSession(); } }); @@ -48,7 +50,7 @@ export default class AuthController { this.renewalTimer = window.setTimeout(() => { this.renewalTimer = null; - this.renew({ userSession }); + this.renew(userSession); }, timeout); } } @@ -68,7 +70,7 @@ export default class AuthController { * return the user session. */ loadUserSession() { - const storedUserSession = localStorage.getItem('userSession'); + const storedUserSession = localStorage.getItem(STORAGE_KEY); const userSession = storedUserSession ? UserSession.deserialize(storedUserSession) : null; @@ -93,9 +95,9 @@ export default class AuthController { */ setUserSession = (userSession) => { if (!userSession) { - localStorage.removeItem('userSession'); + localStorage.removeItem(STORAGE_KEY); } else { - localStorage.setItem('userSession', userSession.serialize()); + localStorage.setItem(STORAGE_KEY, userSession.serialize()); } // localStorage updates do not trigger event listeners on the current window/tab, @@ -104,14 +106,10 @@ export default class AuthController { }; /** - * Renew the user session. This is not possible for all auth methods, and will trivially succeed - * for methods that do not support it. If it fails, the user will be logged out. + * Renew the user session. + * This is not currently supported by the Taskcluster OAuth, so we just clean the session */ - async renew({ userSession }) { - try { - await auth0Renew({ userSession, authController: this }); - } catch (err) { - this.setUserSession(null); - } + async renew() { + this.setUserSession(null); } } diff --git a/src/components/auth/UserSession.js b/src/components/auth/UserSession.js index 26b069b..c4497a4 100644 --- a/src/components/auth/UserSession.js +++ b/src/components/auth/UserSession.js @@ -1,4 +1,5 @@ import { Index } from 'taskcluster-client-web'; +import moment from 'moment'; import { TASKCLUSTER_ROOT_URL } from '../../config'; const USER_ID_REGEX = /mozilla-auth0\/([\w-|]+)\/bugzilla-dashboard-([\w-]+)/; @@ -31,8 +32,18 @@ export default class UserSession { Object.assign(this, options); } - static fromCredentials(credentials) { - return new UserSession({ type: 'credentials', email: 'nobody@mozilla.org', credentials }); + static fromTaskclusterAuth(token, payload) { + // Detect when the credentials will expire + // And substract 1 minute to fetch new credentials before expiry + const expires = moment(payload.expires).subtract(1, 'minute'); + + return new UserSession({ + type: 'credentials', + email: 'nobody@mozilla.org', + renewToken: token, + credentials: payload.credentials, + renewAfter: expires, + }); } // determine whether the user changed from old to new; this is used by other components @@ -52,12 +63,24 @@ export default class UserSession { // get the user's name get name() { return ( - this.fullName - || (this.credentials && this.credentials.clientId) + (this.credentials && this.credentials.clientId) || 'unknown' ); } + // Get the expiry date as a nicely formated string + get expiresIn() { + const diff = moment(this.renewAfter).diff(moment()); + const duration = moment.duration(diff); + if (duration.days() > 0) { + return `${duration.days()} days`; + } + if (duration.hours() > 0) { + return `${duration.hours()} hours`; + } + return `${duration.minutes()} minutes`; + } + get userId() { // Find the user ID in Taskcluster credentials const match = USER_ID_REGEX.exec(this.credentials.clientId); @@ -75,9 +98,7 @@ export default class UserSession { // load Taskcluster credentials for this user getCredentials() { - return this.credentials - ? Promise.resolve(this.credentials) - : this.credentialAgent.getCredentials({}); + return Promise.resolve(this.credentials); } static deserialize(value) { @@ -85,7 +106,7 @@ export default class UserSession { } serialize() { - return JSON.stringify({ ...this, credentialAgent: undefined }); + return JSON.stringify({ ...this }); } getTaskClusterIndexClient = () => new Index({ diff --git a/src/components/auth/oauth2.js b/src/components/auth/oauth2.js index d437e36..edf5613 100644 --- a/src/components/auth/oauth2.js +++ b/src/components/auth/oauth2.js @@ -12,36 +12,15 @@ export function redirectUser() { // Part 2 - Exchange Oauth code for Taskcluster credentials export async function userSessionFromCode(url) { // Get Oauth access token - const user = await webAuth.code.getToken(url); + const token = await webAuth.code.getToken(url); // Exchange that access token for some Taskcluster credentials - const request = user.sign({ + const request = token.sign({ method: 'get', }); const resp = await fetch(config.OAuth2Options.credentialsUri, request); const payload = await resp.json(); // Finally build a new user session - return UserSession.fromCredentials(payload.credentials); -} - -/* eslint-disable consistent-return */ -export async function renew({ userSession }) { - if ( - !userSession - || userSession.type !== 'oidc' - || userSession.oidcProvider !== 'mozilla-auth0' - ) { - return; - } - - return new Promise((accept, reject) => webAuth.renewAuth({}, (err, authResult) => { - if (err) { - return reject(err); - } if (!authResult) { - return reject(new Error('no authResult')); - } - // TODO: somehow renew ? Not possible i think with Oauth + TC - accept(); - })); + return UserSession.fromTaskclusterAuth(token.accessToken, payload); } diff --git a/src/config.js b/src/config.js index 382ec33..596a7b3 100644 --- a/src/config.js +++ b/src/config.js @@ -14,7 +14,9 @@ const config = { redirectUri: PRODUCTION ? 'https://bugzilla-management-dashboard.netlify.app' : 'http://localhost:5000', whitelisted: true, responseType: 'code', - maxExpires: '15 minutes', + query: { + expires: '2 weeks', + }, }, productComponentMetrics: 'project/relman/bugzilla-dashboard/product_component_data.json.gz', reporteesMetrics: 'project/relman/bugzilla-dashboard/reportee_data.json.gz', diff --git a/src/views/ProfileMenu/index.jsx b/src/views/ProfileMenu/index.jsx index 82f5b1e..b6ddaf0 100644 --- a/src/views/ProfileMenu/index.jsx +++ b/src/views/ProfileMenu/index.jsx @@ -72,6 +72,10 @@ class ProfileMenu extends React.Component { onClose={this.handleClose} > {userSession.name} + + Expires in  + {userSession.expiresIn} + context.setUserSession(null)}>Sign out