Skip to content

Commit

Permalink
fix: mfa api token auth requires using authorize call, not checkSessi…
Browse files Browse the repository at this point in the history
…on MP-479
  • Loading branch information
emuvente committed Aug 13, 2024
1 parent 57149f9 commit 56f4fc5
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 114 deletions.
68 changes: 23 additions & 45 deletions src/components/Settings/TwoStepVerification.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,10 @@ import KvSettingsCard from '@/components/Kv/KvSettingsCard';
import KvLoadingPlaceholder from '~/@kiva/kv-components/vue/KvLoadingPlaceholder';
import KvButton from '~/@kiva/kv-components/vue/KvButton';
const pageQuery = gql`query mfaQuery($mfa_token: String!) {
const pageQuery = gql`query mfaQuery {
my {
id
authenticatorEnrollments(mfa_token: $mfa_token) {
id
active
authenticator_type
}
enrolledInMFA
}
}`;
Expand Down Expand Up @@ -85,45 +81,27 @@ export default {
inject: ['apollo', 'kvAuth0'],
mounted() {
this.isLoading = true;
if (this.kvAuth0.enabled) {
this.kvAuth0.checkSession({ skipIfUserExists: true })
.then(() => this.kvAuth0.getMfaManagementToken())
.then(token => {
return this.apollo.query({
query: pageQuery,
variables: {
mfa_token: token
}
});
})
.then(result => {
if (result.errors) {
throw result.errors;
}
const authEnrollments = result.data.my.authenticatorEnrollments;
for (let i = 0; i < authEnrollments.length; i += 1) {
if (authEnrollments[i].active === true) {
this.isMFAActive = true;
this.isLoading = false;
return;
}
}
this.isLoading = false;
})
.catch(err => {
console.error(err);
this.$showTipMsg(
'There was an error when getting your 2-step verification status. '
+ 'Please refresh the page and try again.',
'error'
);
try {
Sentry.captureException(err?.[0]?.extensions?.exception || err);
} catch (e) {
// no-op
}
});
}
this.apollo.query({
query: pageQuery,
}).then(result => {
if (result.errors) {
throw result.errors;
}
this.isMFAActive = result.data.my.enrolledInMFA || false;
this.isLoading = false;
}).catch(err => {
console.error(err);
this.$showTipMsg(
'There was an error when getting your 2-step verification status. '
+ 'Please refresh the page and try again.',
'error'
);
try {
Sentry.captureException(err?.[0]?.extensions?.exception || err);
} catch (e) {
// no-op
}
});
}
};
</script>
37 changes: 8 additions & 29 deletions src/pages/ProcessBrowserAuth.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,21 @@
<script>
import store2 from 'store2';
function checkHashSuccess(hash) {
if (hash.indexOf('error') > -1) {
return false;
}
if (hash.indexOf('access_token') === -1
&& hash.indexOf('id_token') === -1
&& hash.indexOf('refresh_token') === -1) {
return false;
}
return true;
}
export default {
name: 'ProcessBrowserAuth',
inject: ['kvAuth0'],
mounted() {
const hashKey = 'auth0.browser_hash';
const { state } = this.$route.query;
if (state) {
const auth0State = store2.session('auth0.state');
if (auth0State === state) {
const redirect = store2.session('auth0.redirect');
if (window.location.hash) {
const { hash } = window.location;
store2.session.remove('auth0.state');
store2.session.remove('auth0.redirect');
if (checkHashSuccess(hash)) {
// store hash for after post-auth redirect
store2.session(hashKey, hash);
// post-auth redirect
window.location = '/authenticate/ui?doneUrl=/process-browser-auth';
} else {
// some problem occured, so close the window and let normal error handling take over
this.kvAuth0.popupCallback({ hash });
this.$router.push(`${redirect}${window.location.hash}`);
}
} else {
// fetch & erase stored hash
const hash = store2.session(hashKey);
store2.session.remove(hashKey);
// final callback
this.kvAuth0.popupCallback({ hash });
}
},
};
Expand Down
124 changes: 84 additions & 40 deletions src/util/KvAuth0.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ const COOKIE_OPTIONS = { path: '/', secure: true };
// These symbols are unique, and therefore are private to this scope.
// For more details, see https://medium.com/@davidrhyswhite/private-members-in-es6-db1ccd6128a5
const initWebAuth = Symbol('initWebAuth');
const initMfaWebAuth = Symbol('initMfaWebAuth');
const errorCallbacks = Symbol('errorCallbacks');
const handleUnknownError = Symbol('handleUnknownError');
const mfaTokenPromise = Symbol('mfaTokenPromise');
const sessionPromise = Symbol('sessionPromise');
const setAuthData = Symbol('setAuthData');
const setMfaAuthData = Symbol('setMfaAuthData');
const parseMfaHash = Symbol('parseMfaHash');
const noteLoggedIn = Symbol('noteLoggedIn');
const noteLoggedOut = Symbol('noteLoggedOut');
const clearNotedLoginState = Symbol('clearNotedLoginState');
Expand All @@ -32,6 +33,28 @@ function getErrorString(err) {
return `${err.error || err.code || err.name}: ${err.error_description || err.description}`;
}

function isAuth0Hash(hash) {
if (hash.indexOf('error') === -1
&& hash.indexOf('access_token') === -1
&& hash.indexOf('id_token') === -1
&& hash.indexOf('refresh_token') === -1) {
return false;
}
return true;
}

async function storeRedirectState() {
const { default: store2 } = await import('store2');

const { pathname, search } = window.location;
store2.session('auth0.redirect', `${pathname}${search}`);

const state = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
store2.session('auth0.state', state);

return state;
}

// Class to handle interacting with auth0 in the browser
export default class KvAuth0 {
constructor({
Expand Down Expand Up @@ -79,29 +102,9 @@ export default class KvAuth0 {
}
return import('auth0-js').then(({ default: auth0js }) => {
this.webAuth = new auth0js.WebAuth({
audience: this.audience,
clientID: this.clientID,
domain: this.domain,
redirectUri: this.redirectUri,
responseType: 'token id_token',
scope: this.scope,
});
});
}

// Setup Auth0 WebAuth client for MFA management
[initMfaWebAuth]() {
if (this.mfaWebAuth || this.isServer) {
return Promise.resolve();
}
return import('auth0-js').then(({ default: auth0js }) => {
this.mfaWebAuth = new auth0js.WebAuth({
audience: this.mfaAudience,
clientID: this.clientID,
domain: this.domain,
redirectUri: this.redirectUri,
responseType: 'token',
scope: 'enroll read:authenticators remove:authenticators',
});
});
}
Expand All @@ -118,6 +121,17 @@ export default class KvAuth0 {
}
}

[setMfaAuthData]({ accessToken, expiresIn } = {}) {
// save access token as this.mfaManagementToken
this.mfaManagementToken = accessToken;
// mfa management token expiration handling
if (expiresIn > 0) {
setTimeout(() => {
this.mfaManagementToken = '';
}, Number(expiresIn) * 1000);
}
}

/* eslint-disable no-underscore-dangle */

// Return the kiva id for the current user (or undefined)
Expand Down Expand Up @@ -216,6 +230,28 @@ export default class KvAuth0 {
this.cookieStore.remove(SYNC_NAME, COOKIE_OPTIONS);
}

// Parse the hash from the URL to get the MFA management token
[parseMfaHash]() {
return new Promise((resolve, reject) => {
const { hash } = window.location;
if (isAuth0Hash(hash)) {
this.webAuth.parseHash({
hash,
responseType: 'token',
}, (err, result) => {
if (err) {
reject(err);
} else {
this[setMfaAuthData](result);
resolve();
}
});
} else {
resolve();
}
});
}

// Silently fetch an access token for the MFA api to manage MFA factors
getMfaManagementToken() {
// only try this if in the browser
Expand All @@ -236,27 +272,31 @@ export default class KvAuth0 {
return Promise.resolve(this.mfaManagementToken);
}

// Fetch a new mfa management token
this[mfaTokenPromise] = new Promise((resolve, reject) => {
// Ensure browser clock is correct before fetching the token
syncDate().then(() => this[initMfaWebAuth]()).then(() => {
this.mfaWebAuth.checkSession({}, (err, result) => {
if (err) {
reject(err);
} else {
const { accessToken, expiresIn } = result;
// save access token as this.mfaEnrollToken
this.mfaManagementToken = accessToken;
// mfa management token expiration handling
if (expiresIn > 0) {
setTimeout(() => {
this.mfaManagementToken = '';
}, Number(expiresIn) * 1000);
}
// resolve with the mfa management token
// Initialize the web auth client
this[initWebAuth]()
// Parse the hash if it exists
.then(() => this[parseMfaHash]())
.then(() => {
if (this.mfaManagementToken) {
resolve(this.mfaManagementToken);
} else {
// If we still don't have a token, try to get one
// Ensure browser clock is correct before fetching the token
syncDate().then(() => {
// Store the current URL in session storage to redirect back to it after MFA
const state = storeRedirectState();
this.webAuth.authorize({
state,
audience: this.mfaAudience,
responseType: 'token',
scope: 'enroll read:authenticators remove:authenticators',
});
});
}
});
});
})
.catch(reject);
});

// Once the promise completes, stop tracking it
Expand Down Expand Up @@ -285,7 +325,11 @@ export default class KvAuth0 {
this[sessionPromise] = new Promise(resolve => {
// Ensure browser clock is correct before checking session
syncDate().then(() => this[initWebAuth]()).then(() => {
this.webAuth.checkSession({}, (err, result) => {
this.webAuth.checkSession({
audience: this.audience,
responseType: 'token id_token',
scope: this.scope,
}, (err, result) => {
if (err) {
this[setAuthData]();
if (err.error === 'login_required'
Expand Down

0 comments on commit 56f4fc5

Please sign in to comment.