Handle expiry from Taskcluster OAuth (#168)
This commit is contained in:
Родитель
9358f74ac6
Коммит
a420319a8b
|
@ -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 </span>
|
||||||
|
{userSession.expiresIn}
|
||||||
|
</MenuItem>
|
||||||
<MenuItem onClick={() => context.setUserSession(null)}>Sign out</MenuItem>
|
<MenuItem onClick={() => context.setUserSession(null)}>Sign out</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
|
|
Загрузка…
Ссылка в новой задаче