Skip to content

Commit

Permalink
feat: invalid auth_time revoke token behaviour
Browse files Browse the repository at this point in the history
  • Loading branch information
Raphael Kabo committed Jan 23, 2024
1 parent 6a8acc3 commit 3a60ae5
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 2 deletions.
1 change: 1 addition & 0 deletions server/__tests__/middleware/okta.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ jest.mock('@/server/oauth');

const oktaConfig: OktaConfig = {
useOkta: true,
maxAge: 1800,
orgUrl: 'https://example.com',
authServerId: 'foo',
clientId: 'bar',
Expand Down
35 changes: 33 additions & 2 deletions server/middleware/identityMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import {
} from '@/server/middleware/requestMiddleware';
import {
allIdapiCookiesSet,
idTokenIsRecent,
performAuthorizationCodeFlow,
revokeAccessToken,
sanitizeReturnPath,
setLocalStateFromIdTokenOrUserCookie,
verifyOAuthCookiesLocally,
Expand Down Expand Up @@ -97,9 +99,11 @@ export const authenticateWithOAuth = async (
const verifiedTokens = await verifyOAuthCookiesLocally(req);
const guSoTimestamp = parseInt(req.cookies['GU_SO']);
if (requiresSignin(req.originalUrl)) {
console.log(`${returnPath}: Route requires signin`);
// The route requires signin
/////////////////////////////////////////////////////////////////////////////////////
if (verifiedTokens?.accessToken && verifiedTokens?.idToken) {
console.log(`${returnPath}: We have tokens`);
// Check GU_SO cookie timestamp (we want to know if the user has signed out _after_ these tokens were issued,
// so we can redirect them to the OAuth flow to get new tokens for the currently signed-in user (if any).
if (
Expand All @@ -117,21 +121,48 @@ export const authenticateWithOAuth = async (
}
// At this point, the GU_SO cookie is either not set, or it's older than the tokens,
// so we know the tokens belong to the currently signed-in user.

// Check the recency of the ID token (it must be within the configured maxAge in the Okta config).
// If it is not recent, we need to do the following:
// 1. Revoke the access token with Okta
// 2. Clear the OAuth cookies
// 3. Redirect the user to the /reauthenticate route in Gateway, which always shows a sign-in form.
if (!(await idTokenIsRecent(verifiedTokens.idToken))) {
console.log(`${returnPath}: ID token is not recent`);
// 1. Revoke the access token with Okta
await revokeAccessToken(
req.signedCookies[OAuthAccessTokenCookieName],
);
// 2. Clear the OAuth cookies
clearOAuthCookies(res);
// 3. Redirect the user to the /reauthenticate route in Gateway
return res.redirect(
`https://profile.${conf.DOMAIN}/reauthenticate`,
);
}

if (allIdapiCookiesSet(req)) {
console.log(`${returnPath}: All IDAPI cookies are set`);
// The user has valid access and ID tokens, and the full set of IDAPI cookies,
// so they're signed in. We set req.locals.identity so that the frontend can
// correctly show the user as signed in and continue to the route.
setLocalStateFromIdTokenOrUserCookie(
await setLocalStateFromIdTokenOrUserCookie(
req,
res,
verifiedTokens.idToken,
);
console.log(
`${returnPath}: res.locals.identity is now ${JSON.stringify(
res.locals.identity,
)}`,
);
return next();
}
}

// We don't have the tokens, or they're invalid, or we're missing IDAPI cookies,
// so we need to get them.
console.log(`${returnPath}: We do not have tokens`);
return performAuthorizationCodeFlow(req, res, {
redirectUri: `https://manage.${conf.DOMAIN}/oauth/callback`,
scopes,
Expand All @@ -155,7 +186,7 @@ export const authenticateWithOAuth = async (

// Set as much as possible of the local state from the available combination of
// GU_U and the ID token.
setLocalStateFromIdTokenOrUserCookie(
await setLocalStateFromIdTokenOrUserCookie(
req,
res,
verifiedTokens?.idToken,
Expand Down
18 changes: 18 additions & 0 deletions server/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,15 @@ export const performAuthorizationCodeFlow = async (
return res.redirect(303, authorizeUrl);
};

export const revokeAccessToken = async (token: string) => {
const openIdClient = await getOpenIdClient();
try {
await openIdClient.revoke(token, 'access_token');
} catch (error) {
log.error('OAuth / Revoke Access Token / Error', error);
}
};

/**
* @name verifyOAuthCookiesLocally
*
Expand Down Expand Up @@ -327,19 +336,28 @@ export const idTokenIsRecent = async (idToken: OktaJwtVerifier.Jwt) => {
const { maxAge } = await getOktaConfig();
const authTime = idToken.claims.auth_time;

console.log(
`Comparing ${nowSeconds} and ${authTime} with maxAge ${maxAge} = ${
nowSeconds - authTime

Check failure on line 341 in server/oauth.ts

View workflow job for this annotation

GitHub Actions / manage-frontend build

Object is of type 'unknown'.
}`,
);

if (
!authTime ||
typeof authTime !== 'number' ||
authTime < 0 ||
authTime > nowSeconds
) {
console.log(' ID token does not have a valid auth_time claim');
return false;
}

if (nowSeconds - authTime > maxAge) {
console.log(' ID token is not recent');
return false;
}

console.log(' ID token is recent');
return true;
};

Expand Down

0 comments on commit 3a60ae5

Please sign in to comment.