Skip to content

Commit

Permalink
tests: unit tests for auth_time handling methods
Browse files Browse the repository at this point in the history
  • Loading branch information
Raphael Kabo committed Jan 24, 2024
1 parent 1e8ad23 commit 9723455
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 13 deletions.
54 changes: 54 additions & 0 deletions server/__tests__/middleware/identityMiddleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

import type { Jwt, JwtClaims } from '@okta/jwt-verifier';
import type OktaJwtVerifier from '@okta/jwt-verifier';
import type { Request, Response } from 'express';
import { conf } from '@/server/config';
import { authenticateWithOAuth } from '@/server/middleware/identityMiddleware';
Expand Down Expand Up @@ -30,6 +31,8 @@ jest.mock('@/server/oauth', () => ({
verifyIdToken: jest.fn(),
verifyAccessToken: jest.fn(),
setLocalStateFromIdTokenOrUserCookie: jest.fn(),
idTokenIsRecent: jest.fn(),
revokeAccessToken: jest.fn(),
}));
const mockedVerifyOAuthCookiesLocally = jest.mocked<
(req: Request) => Promise<VerifiedOAuthCookies | undefined>
Expand All @@ -40,6 +43,12 @@ const mockedVerifyIdToken = jest.mocked<
const mockedVerifyAccessToken = jest.mocked<
(token: string) => Promise<Jwt | undefined>
>(oauth.verifyAccessToken);
const mockedIdTokenIsRecent = jest.mocked<
(token: OktaJwtVerifier.Jwt) => Promise<boolean>
>(oauth.idTokenIsRecent);
const mockedRevokeAccessToken = jest.mocked<(token: string) => Promise<void>>(
oauth.revokeAccessToken,
);

describe('authenticateWithOAuth middleware - route requires signin', () => {
beforeEach(() => {
Expand Down Expand Up @@ -121,6 +130,8 @@ describe('authenticateWithOAuth middleware - route requires signin', () => {
}),
);

mockedIdTokenIsRecent.mockResolvedValue(true);

const next = jest.fn();

await authenticateWithOAuth(req, res as unknown as Response, next);
Expand Down Expand Up @@ -158,6 +169,7 @@ describe('authenticateWithOAuth middleware - route requires signin', () => {
mockedVerifyIdToken.mockResolvedValue({
isExpired: () => true,
} as Jwt);
mockedIdTokenIsRecent.mockResolvedValue(true);
const next = jest.fn();
await authenticateWithOAuth(req, res as unknown as Response, next);
expect(oauth.performAuthorizationCodeFlow).toHaveBeenCalledWith(
Expand Down Expand Up @@ -198,6 +210,7 @@ describe('authenticateWithOAuth middleware - route requires signin', () => {
},
} as Jwt);
mockedVerifyIdToken.mockResolvedValue(idToken);
mockedIdTokenIsRecent.mockResolvedValue(true);
const next = jest.fn();
await authenticateWithOAuth(req, res as unknown as Response, next);
expect(oauth.performAuthorizationCodeFlow).toHaveBeenCalledWith(
Expand Down Expand Up @@ -246,6 +259,7 @@ describe('authenticateWithOAuth middleware - route requires signin', () => {
idToken: idToken as unknown as Jwt,
}),
);
mockedIdTokenIsRecent.mockResolvedValue(true);
const next = jest.fn();
await authenticateWithOAuth(req, res as unknown as Response, next);
expect(oauth.setLocalStateFromIdTokenOrUserCookie).toHaveBeenCalledWith(
Expand All @@ -255,6 +269,46 @@ describe('authenticateWithOAuth middleware - route requires signin', () => {
);
expect(next).toHaveBeenCalled();
});

it('revokes and deletes access token and redirects to /reauthenticate if the ID token is expired', async () => {
const req = {
signedCookies: {},
cookies: {
GU_SO: '1000',
},
originalUrl: '/profile',
} as Request;

const res = {
clearCookie: jest.fn(),
redirect: jest.fn(),
};

mockedVerifyOAuthCookiesLocally.mockReturnValue(
Promise.resolve({
accessToken: {
claims: {
iat: 2000,
} as JwtClaims,
} as Jwt,
idToken: {} as Jwt,
}),
);

mockedIdTokenIsRecent.mockResolvedValue(false);

const next = jest.fn();

await authenticateWithOAuth(req, res as unknown as Response, next);

expect(mockedRevokeAccessToken).toHaveBeenCalled();
expect(res.clearCookie).toHaveBeenCalledTimes(2);
expect(res.redirect).toHaveBeenCalledWith(
expect.stringContaining('/reauthenticate'),
);
expect(oauth.performAuthorizationCodeFlow).not.toHaveBeenCalled();
expect(next).not.toHaveBeenCalled();
});
});

describe('authenticateWithOAuth middleware - route does not require signin', () => {
Expand Down
158 changes: 150 additions & 8 deletions server/__tests__/oauth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jest.mock('@/server/oktaConfig', () => ({
orgUrl: 'https://example.com',
authServerId: 'foo',
clientId: 'bar',
maxAge: 1800,
}),
}));

Expand Down Expand Up @@ -149,7 +150,11 @@ describe('verifyOAuthCookiesLocally', () => {
});

describe('setLocalStateFromIdTokenOrUserCookie', () => {
it('sets the local state from the ID token if it exists', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('sets the local state from the ID token if it exists', async () => {
const req = {
cookies: {
GU_U: 'gu_u',
Expand All @@ -162,6 +167,8 @@ describe('setLocalStateFromIdTokenOrUserCookie', () => {
.spyOn(identityLocalState, 'setIdentityLocalState')
.mockImplementation();

jest.spyOn(oauth, 'signInStatus').mockResolvedValue('signedInRecently');

const idToken = {
claims: {
legacy_identity_id: 'legacy_identity_id',
Expand All @@ -182,7 +189,7 @@ describe('setLocalStateFromIdTokenOrUserCookie', () => {
isNotBefore: () => false,
} as Jwt;

oauth.setLocalStateFromIdTokenOrUserCookie(req, res, idToken);
await oauth.setLocalStateFromIdTokenOrUserCookie(req, res, idToken);

expect(spyOnSetIdentityLocalState).toHaveBeenCalledWith(res, {
signInStatus: 'signedInRecently',
Expand All @@ -192,7 +199,7 @@ describe('setLocalStateFromIdTokenOrUserCookie', () => {
});
});

it('sets the local state from the GU_U cookie if it exists', () => {
it('sets the local state from the GU_U cookie if it exists', async () => {
const req = {
cookies: {
GU_U: 'gu_u',
Expand All @@ -205,14 +212,18 @@ describe('setLocalStateFromIdTokenOrUserCookie', () => {
.spyOn(identityLocalState, 'setIdentityLocalState')
.mockImplementation();

oauth.setLocalStateFromIdTokenOrUserCookie(req, res);
jest.spyOn(oauth, 'signInStatus').mockResolvedValue(
'signedInNotRecently',
);

await oauth.setLocalStateFromIdTokenOrUserCookie(req, res);

expect(spyOnSetIdentityLocalState).toHaveBeenCalledWith(res, {
signInStatus: 'signedInRecently',
signInStatus: 'signedInNotRecently',
});
});

it("does not set 'signedInRecently' if neither the ID token nor the GU_U cookie exist", () => {
it("does not set 'signedInRecently' if neither the ID token nor the GU_U cookie exist", async () => {
const req = {
cookies: {
SC_GU_U: 'sc_gu_u',
Expand All @@ -224,8 +235,139 @@ describe('setLocalStateFromIdTokenOrUserCookie', () => {
.spyOn(identityLocalState, 'setIdentityLocalState')
.mockImplementation();

oauth.setLocalStateFromIdTokenOrUserCookie(req, res);
jest.spyOn(oauth, 'signInStatus').mockResolvedValue('notSignedIn');

await oauth.setLocalStateFromIdTokenOrUserCookie(req, res);

expect(spyOnSetIdentityLocalState).toHaveBeenCalledWith(res, {
displayName: undefined,
email: undefined,
signInStatus: 'notSignedIn',
userId: undefined,
});
});

afterAll(() => {
// Needed because we're mocking signInStatus here but using it further down
jest.restoreAllMocks();
});
});

describe('signInStatus', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('returns signedInRecently if the ID token is recent', async () => {
jest.spyOn(oauth, 'idTokenIsRecent').mockResolvedValue(true);
const idToken = {} as Jwt;
const guUCookie = 1234567890;

const status = await oauth.signInStatus(idToken, guUCookie);

expect(status).toEqual('signedInRecently');
});

it('returns signedInNotRecently if the ID token is not recent', async () => {
jest.spyOn(oauth, 'idTokenIsRecent').mockResolvedValue(false);
const idToken = {} as Jwt;
const guUCookie = 1234567890;

const status = await oauth.signInStatus(idToken, guUCookie);

expect(status).toEqual('signedInNotRecently');
});
it('returns signedInNotRecently if the GU_U cookie exists, but the ID token does not', async () => {
const guUCookie = 1234567890;

const status = await oauth.signInStatus(undefined, guUCookie);

expect(status).toEqual('signedInNotRecently');
});
it('returns notSignedIn if neither the GU_U cookie nor the ID token exist', async () => {
const status = await oauth.signInStatus(undefined, undefined);

expect(status).toEqual('notSignedIn');
});

afterAll(() => {
// Needed because we're mocking idTokenIsRecent here but using it further down
jest.restoreAllMocks();
});
});

describe('idTokenIsRecent', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('returns true if the ID token is not expired', async () => {
const idToken = {
claims: {
auth_time: Date.now() / 1000 - 100,
},
} as unknown as Jwt;

const isRecent = await oauth.idTokenIsRecent(idToken);

expect(isRecent).toEqual(true);
});

it('returns false if the ID token does not have an auth_time claim', async () => {
const idToken = {
claims: {},
} as unknown as Jwt;

const isRecent = await oauth.idTokenIsRecent(idToken);

expect(isRecent).toEqual(false);
});

it('returns false if the ID token has an auth_time claim that is not a number', async () => {
const idToken = {
claims: {
auth_time: 'foo',
},
} as unknown as Jwt;

const isRecent = await oauth.idTokenIsRecent(idToken);

expect(isRecent).toEqual(false);
});

it('returns false if the ID token has an auth_time claim that is less than 0', async () => {
const idToken = {
claims: {
auth_time: -1,
},
} as unknown as Jwt;

const isRecent = await oauth.idTokenIsRecent(idToken);

expect(isRecent).toEqual(false);
});

it('returns false if the ID token has an auth_time claim that is greater than the current time', async () => {
const idToken = {
claims: {
auth_time: Date.now() / 1000 + 100,
},
} as unknown as Jwt;

const isRecent = await oauth.idTokenIsRecent(idToken);

expect(isRecent).toEqual(false);
});

it('returns false if the ID token is expired', async () => {
const idToken = {
claims: {
auth_time: Date.now() / 1000 - 10000,
},
} as unknown as Jwt;

const isRecent = await oauth.idTokenIsRecent(idToken);

expect(spyOnSetIdentityLocalState).toHaveBeenCalledWith(res, {});
expect(isRecent).toEqual(false);
});
});
4 changes: 2 additions & 2 deletions server/middleware/identityMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const handleIdentityMiddlewareError = (err: Error, res: Response) => {
res.redirect('/maintenance');
};

const clearOAuthCookies = (res: Response) => {
export const clearOAuthCookies = (res: Response) => {
res.clearCookie(OAuthAccessTokenCookieName, oauthCookieOptions);
res.clearCookie(OAuthIdTokenCookieName, oauthCookieOptions);
};
Expand Down Expand Up @@ -153,7 +153,7 @@ export const authenticateWithOAuth = async (
);
console.log(
`${returnPath}: res.locals.identity is now ${JSON.stringify(
res.locals.identity,
res.locals?.identity,
)}`,
);
return next();
Expand Down
6 changes: 3 additions & 3 deletions server/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,13 +367,13 @@ export const idTokenIsRecent = async (idToken: OktaJwtVerifier.Jwt) => {
* @description Returns one of three values:
* - 'signedInRecently' if the user has a recent ID token.
* - 'signedInNotRecently' if the user has a valid ID token but it's not recent,
* or if the user has a GU_U cookie.
* - 'notSignedIn' if the user has no valid ID token or GU_U cookie.
* or if the user has only a GU_U cookie, but no ID token.
* - 'notSignedIn' if the user has no ID token or GU_U cookie.
*
* @param idToken - the ID token to check (optional)
* @param guUCookie - the GU_U cookie to check (optional)
*/
const signInStatus = async (
export const signInStatus = async (
idToken: OktaJwtVerifier.Jwt | undefined,
guUCookie: number | undefined,
) => {
Expand Down

0 comments on commit 9723455

Please sign in to comment.