-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(adapter-nextjs): create jwt verifier once (#13825)
* fix: create validator once * fix: move token validator initialization to createRunWithAmplifyServerContext * Fix logic issue * Fixed outdated comments --------- Co-authored-by: Chris Fang <cshfang@gmail.com> Co-authored-by: Chris F <5827964+cshfang@users.noreply.github.com>
- Loading branch information
1 parent
ec2ff53
commit 88f9eef
Showing
9 changed files
with
199 additions
and
162 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
151 changes: 87 additions & 64 deletions
151
packages/adapter-nextjs/__tests__/utils/createTokenValidator.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,85 +1,108 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import { CognitoJwtVerifier } from 'aws-jwt-verify'; | ||
|
||
import { isValidCognitoToken } from '../../src/utils/isValidCognitoToken'; | ||
import { createTokenValidator } from '../../src/utils/createTokenValidator'; | ||
import { JwtVerifier } from '../../src/types'; | ||
|
||
jest.mock('aws-jwt-verify'); | ||
jest.mock('../../src/utils/isValidCognitoToken'); | ||
|
||
const mockIsValidCognitoToken = isValidCognitoToken as jest.Mock; | ||
|
||
const userPoolId = 'userPoolId'; | ||
const userPoolClientId = 'clientId'; | ||
const tokenValidatorInput = { | ||
userPoolId, | ||
userPoolClientId, | ||
}; | ||
const accessToken = { | ||
key: 'CognitoIdentityServiceProvider.clientId.usersub.accessToken', | ||
value: | ||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMTEiLCJpc3MiOiJodHRwc', | ||
}; | ||
const idToken = { | ||
key: 'CognitoIdentityServiceProvider.clientId.usersub.idToken', | ||
value: 'eyJzdWIiOiIxMTEiLCJpc3MiOiJodHRwc.XAiOiJKV1QiLCJhbGciOiJIUzI1NiJ', | ||
}; | ||
|
||
const tokenValidator = createTokenValidator({ | ||
userPoolId, | ||
userPoolClientId, | ||
}); | ||
describe('createTokenValidator', () => { | ||
const userPoolId = 'userPoolId'; | ||
const userPoolClientId = 'clientId'; | ||
const accessToken = { | ||
key: 'CognitoIdentityServiceProvider.clientId.usersub.accessToken', | ||
value: 'access-token-value', | ||
}; | ||
const idToken = { | ||
key: 'CognitoIdentityServiceProvider.clientId.usersub.idToken', | ||
value: 'id-token-value', | ||
}; | ||
|
||
const mockIsValidCognitoToken = jest.mocked(isValidCognitoToken); | ||
const mockCognitoJwtVerifier = { | ||
create: jest.mocked(CognitoJwtVerifier.create), | ||
}; | ||
|
||
describe('Validator', () => { | ||
afterEach(() => { | ||
jest.resetAllMocks(); | ||
}); | ||
it('should return a validator', () => { | ||
expect(createTokenValidator(tokenValidatorInput)).toBeDefined(); | ||
mockIsValidCognitoToken.mockClear(); | ||
}); | ||
|
||
it('should return true for non-token keys', async () => { | ||
const result = await tokenValidator.getItem?.('mockKey', 'mockValue'); | ||
expect(result).toBe(true); | ||
expect(mockIsValidCognitoToken).toHaveBeenCalledTimes(0); | ||
it('should return a token validator', () => { | ||
expect( | ||
createTokenValidator({ | ||
userPoolId, | ||
userPoolClientId, | ||
}), | ||
).toStrictEqual({ | ||
getItem: expect.any(Function), | ||
}); | ||
}); | ||
|
||
it('should return true for valid accessToken', async () => { | ||
mockIsValidCognitoToken.mockImplementation(() => Promise.resolve(true)); | ||
|
||
const result = await tokenValidator.getItem?.( | ||
accessToken.key, | ||
accessToken.value, | ||
); | ||
|
||
expect(result).toBe(true); | ||
expect(mockIsValidCognitoToken).toHaveBeenCalledTimes(1); | ||
expect(mockIsValidCognitoToken).toHaveBeenCalledWith({ | ||
userPoolId, | ||
clientId: userPoolClientId, | ||
token: accessToken.value, | ||
tokenType: 'access', | ||
describe('created token validator', () => { | ||
afterEach(() => { | ||
mockCognitoJwtVerifier.create.mockReset(); | ||
}); | ||
}); | ||
|
||
it('should return true for valid idToken', async () => { | ||
mockIsValidCognitoToken.mockImplementation(() => Promise.resolve(true)); | ||
|
||
const result = await tokenValidator.getItem?.(idToken.key, idToken.value); | ||
expect(result).toBe(true); | ||
expect(mockIsValidCognitoToken).toHaveBeenCalledTimes(1); | ||
expect(mockIsValidCognitoToken).toHaveBeenCalledWith({ | ||
userPoolId, | ||
clientId: userPoolClientId, | ||
token: idToken.value, | ||
tokenType: 'id', | ||
it('should return true if key is not for access or id tokens', async () => { | ||
const tokenValidator = createTokenValidator({ | ||
userPoolId, | ||
userPoolClientId, | ||
}); | ||
|
||
expect(await tokenValidator.getItem?.('key', 'value')).toBe(true); | ||
expect(mockIsValidCognitoToken).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
it('should return false if invalid tokenType is access', async () => { | ||
mockIsValidCognitoToken.mockImplementation(() => Promise.resolve(false)); | ||
it('should return false if validator created without user pool or client ids', async () => { | ||
const tokenValidator = createTokenValidator({}); | ||
|
||
const result = await tokenValidator.getItem?.(idToken.key, idToken.value); | ||
expect(result).toBe(false); | ||
expect(mockIsValidCognitoToken).toHaveBeenCalledTimes(1); | ||
expect( | ||
await tokenValidator.getItem?.(accessToken.key, accessToken.value), | ||
).toBe(false); | ||
expect(await tokenValidator.getItem?.(idToken.key, idToken.value)).toBe( | ||
false, | ||
); | ||
expect(mockIsValidCognitoToken).not.toHaveBeenCalled(); | ||
}); | ||
|
||
describe.each([ | ||
{ tokenUse: 'access', token: accessToken }, | ||
{ tokenUse: 'id', token: idToken }, | ||
])('$tokenUse token verifier', ({ tokenUse, token }) => { | ||
const mockTokenVerifier = {} as JwtVerifier; | ||
const tokenValidator = createTokenValidator({ | ||
userPoolId, | ||
userPoolClientId, | ||
}); | ||
|
||
beforeAll(() => { | ||
mockCognitoJwtVerifier.create.mockReturnValue(mockTokenVerifier); | ||
}); | ||
|
||
it('should create a jwt verifier and use it to validate', async () => { | ||
await tokenValidator.getItem?.(token.key, token.value); | ||
|
||
expect(mockCognitoJwtVerifier.create).toHaveBeenCalledWith({ | ||
userPoolId, | ||
clientId: userPoolClientId, | ||
tokenUse, | ||
}); | ||
expect(mockIsValidCognitoToken).toHaveBeenCalledWith({ | ||
token: token.value, | ||
verifier: mockTokenVerifier, | ||
}); | ||
}); | ||
|
||
it('should not re-create the jwt verifier', async () => { | ||
await tokenValidator.getItem?.(token.key, token.value); | ||
|
||
expect(mockCognitoJwtVerifier.create).not.toHaveBeenCalled(); | ||
expect(mockIsValidCognitoToken).toHaveBeenCalled(); | ||
}); | ||
}); | ||
}); | ||
}); |
95 changes: 29 additions & 66 deletions
95
packages/adapter-nextjs/__tests__/utils/isValidCognitoToken.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,94 +1,57 @@ | ||
import { CognitoJwtVerifier } from 'aws-jwt-verify'; | ||
import { JwtExpiredError } from 'aws-jwt-verify/error'; | ||
|
||
import { isValidCognitoToken } from '../../src/utils/isValidCognitoToken'; | ||
|
||
jest.mock('aws-jwt-verify', () => { | ||
return { | ||
CognitoJwtVerifier: { | ||
create: jest.fn(), | ||
}, | ||
}; | ||
}); | ||
|
||
const mockedCreate = CognitoJwtVerifier.create as jest.MockedFunction< | ||
typeof CognitoJwtVerifier.create | ||
>; | ||
import { JwtVerifier } from '../../src/types'; | ||
|
||
describe('isValidCognitoToken', () => { | ||
const token = 'mocked-token'; | ||
const userPoolId = 'us-east-1_test'; | ||
const clientId = 'client-id-test'; | ||
const tokenType = 'id'; | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('should return true for a valid token', async () => { | ||
const mockVerifier: any = { | ||
verify: jest.fn().mockResolvedValue({}), | ||
// @ts-expect-error - partial mock | ||
const mockVerifier: JwtVerifier = { | ||
verify: jest.fn().mockResolvedValue(null), | ||
}; | ||
mockedCreate.mockReturnValue(mockVerifier); | ||
|
||
const isValid = await isValidCognitoToken({ | ||
token, | ||
userPoolId, | ||
clientId, | ||
tokenType, | ||
}); | ||
expect(isValid).toBe(true); | ||
expect(CognitoJwtVerifier.create).toHaveBeenCalledWith({ | ||
userPoolId, | ||
clientId, | ||
tokenUse: tokenType, | ||
}); | ||
expect( | ||
await isValidCognitoToken({ | ||
token, | ||
verifier: mockVerifier, | ||
}), | ||
).toBe(true); | ||
expect(mockVerifier.verify).toHaveBeenCalledWith(token); | ||
}); | ||
|
||
it('should return true for a token that has valid signature and expired', async () => { | ||
const mockVerifier: any = { | ||
it('should return true for a token that has valid signature but is expired', async () => { | ||
// @ts-expect-error - partial mock | ||
const mockVerifier: JwtVerifier = { | ||
verify: jest | ||
.fn() | ||
.mockRejectedValue( | ||
new JwtExpiredError('Token expired', 'mocked-token'), | ||
), | ||
.mockRejectedValue(new JwtExpiredError('Token expired', token)), | ||
}; | ||
mockedCreate.mockReturnValue(mockVerifier); | ||
|
||
const isValid = await isValidCognitoToken({ | ||
token, | ||
userPoolId, | ||
clientId, | ||
tokenType, | ||
}); | ||
expect(isValid).toBe(true); | ||
expect(CognitoJwtVerifier.create).toHaveBeenCalledWith({ | ||
userPoolId, | ||
clientId, | ||
tokenUse: tokenType, | ||
}); | ||
expect(mockVerifier.verify).toHaveBeenCalledWith(token); | ||
expect( | ||
await isValidCognitoToken({ | ||
token, | ||
verifier: mockVerifier, | ||
}), | ||
).toBe(true); | ||
}); | ||
|
||
it('should return false for an invalid token', async () => { | ||
const mockVerifier: any = { | ||
verify: jest.fn().mockRejectedValue(new Error('Invalid token')), | ||
// @ts-expect-error - partial mock | ||
const mockVerifier: JwtVerifier = { | ||
verify: jest.fn().mockRejectedValue(null), | ||
}; | ||
mockedCreate.mockReturnValue(mockVerifier); | ||
|
||
const isValid = await isValidCognitoToken({ | ||
token, | ||
userPoolId, | ||
clientId, | ||
tokenType, | ||
}); | ||
expect(isValid).toBe(false); | ||
expect(CognitoJwtVerifier.create).toHaveBeenCalledWith({ | ||
userPoolId, | ||
clientId, | ||
tokenUse: tokenType, | ||
}); | ||
expect(mockVerifier.verify).toHaveBeenCalledWith(token); | ||
expect( | ||
await isValidCognitoToken({ | ||
token, | ||
verifier: mockVerifier, | ||
}), | ||
).toBe(false); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.