Handle expiry from Taskcluster OAuth (#168)

This commit is contained in:
Bastien Abadie 2020-04-20 13:50:27 +02:00 коммит произвёл GitHub
Родитель 9358f74ac6
Коммит a420319a8b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 51 добавлений и 47 удалений

Просмотреть файл

@ -1,7 +1,9 @@
import mitt from 'mitt'; import mitt from 'mitt';
import UserSession from './UserSession'; 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. * Controller for authentication-related pieces of the site.
@ -21,7 +23,7 @@ export default class AuthController {
this.renewalTimer = null; this.renewalTimer = null;
window.addEventListener('storage', ({ storageArea, key }) => { window.addEventListener('storage', ({ storageArea, key }) => {
if (storageArea === localStorage && key === 'userSession') { if (storageArea === localStorage && key === STORAGE_KEY) {
this.loadUserSession(); this.loadUserSession();
} }
}); });
@ -48,7 +50,7 @@ export default class AuthController {
this.renewalTimer = window.setTimeout(() => { this.renewalTimer = window.setTimeout(() => {
this.renewalTimer = null; this.renewalTimer = null;
this.renew({ userSession }); this.renew(userSession);
}, timeout); }, timeout);
} }
} }
@ -68,7 +70,7 @@ export default class AuthController {
* return the user session. * return the user session.
*/ */
loadUserSession() { loadUserSession() {
const storedUserSession = localStorage.getItem('userSession'); const storedUserSession = localStorage.getItem(STORAGE_KEY);
const userSession = storedUserSession const userSession = storedUserSession
? UserSession.deserialize(storedUserSession) ? UserSession.deserialize(storedUserSession)
: null; : null;
@ -93,9 +95,9 @@ export default class AuthController {
*/ */
setUserSession = (userSession) => { setUserSession = (userSession) => {
if (!userSession) { if (!userSession) {
localStorage.removeItem('userSession'); localStorage.removeItem(STORAGE_KEY);
} else { } else {
localStorage.setItem('userSession', userSession.serialize()); localStorage.setItem(STORAGE_KEY, userSession.serialize());
} }
// localStorage updates do not trigger event listeners on the current window/tab, // 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 * Renew the user session.
* for methods that do not support it. If it fails, the user will be logged out. * This is not currently supported by the Taskcluster OAuth, so we just clean the session
*/ */
async renew({ userSession }) { async renew() {
try { this.setUserSession(null);
await auth0Renew({ userSession, authController: this });
} catch (err) {
this.setUserSession(null);
}
} }
} }

Просмотреть файл

@ -1,4 +1,5 @@
import { Index } from 'taskcluster-client-web'; import { Index } from 'taskcluster-client-web';
import moment from 'moment';
import { TASKCLUSTER_ROOT_URL } from '../../config'; import { TASKCLUSTER_ROOT_URL } from '../../config';
const USER_ID_REGEX = /mozilla-auth0\/([\w-|]+)\/bugzilla-dashboard-([\w-]+)/; const USER_ID_REGEX = /mozilla-auth0\/([\w-|]+)\/bugzilla-dashboard-([\w-]+)/;
@ -31,8 +32,18 @@ export default class UserSession {
Object.assign(this, options); Object.assign(this, options);
} }
static fromCredentials(credentials) { static fromTaskclusterAuth(token, payload) {
return new UserSession({ type: 'credentials', email: 'nobody@mozilla.org', credentials }); // 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 // 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 the user's name
get name() { get name() {
return ( return (
this.fullName (this.credentials && this.credentials.clientId)
|| (this.credentials && this.credentials.clientId)
|| 'unknown' || '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() { get userId() {
// Find the user ID in Taskcluster credentials // Find the user ID in Taskcluster credentials
const match = USER_ID_REGEX.exec(this.credentials.clientId); const match = USER_ID_REGEX.exec(this.credentials.clientId);
@ -75,9 +98,7 @@ export default class UserSession {
// load Taskcluster credentials for this user // load Taskcluster credentials for this user
getCredentials() { getCredentials() {
return this.credentials return Promise.resolve(this.credentials);
? Promise.resolve(this.credentials)
: this.credentialAgent.getCredentials({});
} }
static deserialize(value) { static deserialize(value) {
@ -85,7 +106,7 @@ export default class UserSession {
} }
serialize() { serialize() {
return JSON.stringify({ ...this, credentialAgent: undefined }); return JSON.stringify({ ...this });
} }
getTaskClusterIndexClient = () => new Index({ getTaskClusterIndexClient = () => new Index({

Просмотреть файл

@ -12,36 +12,15 @@ export function redirectUser() {
// Part 2 - Exchange Oauth code for Taskcluster credentials // Part 2 - Exchange Oauth code for Taskcluster credentials
export async function userSessionFromCode(url) { export async function userSessionFromCode(url) {
// Get Oauth access token // 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 // Exchange that access token for some Taskcluster credentials
const request = user.sign({ const request = token.sign({
method: 'get', method: 'get',
}); });
const resp = await fetch(config.OAuth2Options.credentialsUri, request); const resp = await fetch(config.OAuth2Options.credentialsUri, request);
const payload = await resp.json(); const payload = await resp.json();
// Finally build a new user session // Finally build a new user session
return UserSession.fromCredentials(payload.credentials); return UserSession.fromTaskclusterAuth(token.accessToken, payload);
}
/* 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();
}));
} }

Просмотреть файл

@ -14,7 +14,9 @@ const config = {
redirectUri: PRODUCTION ? 'https://bugzilla-management-dashboard.netlify.app' : 'http://localhost:5000', redirectUri: PRODUCTION ? 'https://bugzilla-management-dashboard.netlify.app' : 'http://localhost:5000',
whitelisted: true, whitelisted: true,
responseType: 'code', responseType: 'code',
maxExpires: '15 minutes', query: {
expires: '2 weeks',
},
}, },
productComponentMetrics: 'project/relman/bugzilla-dashboard/product_component_data.json.gz', productComponentMetrics: 'project/relman/bugzilla-dashboard/product_component_data.json.gz',
reporteesMetrics: 'project/relman/bugzilla-dashboard/reportee_data.json.gz', reporteesMetrics: 'project/relman/bugzilla-dashboard/reportee_data.json.gz',

Просмотреть файл

@ -72,6 +72,10 @@ class ProfileMenu extends React.Component {
onClose={this.handleClose} onClose={this.handleClose}
> >
<MenuItem>{userSession.name}</MenuItem> <MenuItem>{userSession.name}</MenuItem>
<MenuItem>
<span>Expires in&nbsp;</span>
{userSession.expiresIn}
</MenuItem>
<MenuItem onClick={() => context.setUserSession(null)}>Sign out</MenuItem> <MenuItem onClick={() => context.setUserSession(null)}>Sign out</MenuItem>
</Menu> </Menu>
</div> </div>