Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update scope cookie to accurately reflect user scopes in access… #102

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions __tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@ describe('private functions', () => {

test('should getRedirectResponse', async () => {
const username = 'toto';
const scope = 'phone email profile openid aws.cognito.signin.user.admin';
const domain = 'example.com';
const path = '/test';
jest.spyOn(authenticator._jwtVerifier, 'verify');
authenticator._jwtVerifier.verify.mockReturnValueOnce(Promise.resolve({ token_use: 'id', 'cognito:username': username }));
authenticator._jwtVerifier.verify.mockReturnValueOnce(Promise.resolve({ token_use: 'access', 'scope': scope }));

const response = await authenticator._getRedirectResponse({refreshToken: tokenData.refresh_token, accessToken: tokenData.access_token, idToken: tokenData.id_token}, domain, path);
expect(response).toMatchObject({
Expand All @@ -70,7 +72,7 @@ describe('private functions', () => {
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.idToken=${tokenData.id_token}; Domain=${domain}; Expires=${DATE.toUTCString()}; Secure`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.LastAuthUser=${username}; Domain=${domain}; Expires=${DATE.toUTCString()}; Secure`},
]));
expect(authenticator._jwtVerifier.verify).toHaveBeenCalled();
expect(authenticator._jwtVerifier.verify).toHaveBeenCalledTimes(2);
});

test('should not return cookie domain', async () => {
Expand All @@ -85,10 +87,12 @@ describe('private functions', () => {
authenticatorWithNoCookieDomain._jwtVerifier.cacheJwks(jwksData);

const username = 'toto';
const scope = 'phone email profile openid aws.cognito.signin.user.admin';
const domain = 'example.com';
const path = '/test';
jest.spyOn(authenticatorWithNoCookieDomain._jwtVerifier, 'verify');
authenticatorWithNoCookieDomain._jwtVerifier.verify.mockReturnValueOnce(Promise.resolve({ token_use: 'id', 'cognito:username': username }));
authenticatorWithNoCookieDomain._jwtVerifier.verify.mockReturnValueOnce(Promise.resolve({ token_use: 'access', 'scope': scope }));

const response = await authenticatorWithNoCookieDomain._getRedirectResponse({'accessToken': tokenData.access_token, 'idToken': tokenData.id_token, 'refreshToken': tokenData.refresh_token}, domain, path);
expect(response).toMatchObject({
Expand All @@ -107,7 +111,7 @@ describe('private functions', () => {
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.idToken=${tokenData.id_token}; Expires=${DATE.toUTCString()}; Secure`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.LastAuthUser=${username}; Expires=${DATE.toUTCString()}; Secure`},
]));
expect(authenticatorWithNoCookieDomain._jwtVerifier.verify).toHaveBeenCalled();
expect(authenticatorWithNoCookieDomain._jwtVerifier.verify).toHaveBeenCalledTimes(2);
});

test('should set HttpOnly on cookies', async () => {
Expand All @@ -123,10 +127,12 @@ describe('private functions', () => {
authenticatorWithHttpOnly._jwtVerifier.cacheJwks(jwksData);

const username = 'toto';
const scope = 'phone email profile openid aws.cognito.signin.user.admin';
const domain = 'example.com';
const path = '/test';
jest.spyOn(authenticatorWithHttpOnly._jwtVerifier, 'verify');
authenticatorWithHttpOnly._jwtVerifier.verify.mockReturnValueOnce(Promise.resolve({ token_use: 'id', 'cognito:username': username }));
authenticatorWithHttpOnly._jwtVerifier.verify.mockReturnValueOnce(Promise.resolve({ token_use: 'access', 'scope': scope }));

const response = await authenticatorWithHttpOnly._getRedirectResponse({ accessToken: tokenData.access_token, idToken: tokenData.id_token, refreshToken: tokenData.refresh_token }, domain, path);
expect(response).toMatchObject({
Expand All @@ -145,7 +151,7 @@ describe('private functions', () => {
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.idToken=${tokenData.id_token}; Domain=${domain}; Expires=${DATE.toUTCString()}; Secure; HttpOnly`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.LastAuthUser=${username}; Domain=${domain}; Expires=${DATE.toUTCString()}; Secure; HttpOnly`},
]));
expect(authenticatorWithHttpOnly._jwtVerifier.verify).toHaveBeenCalled();
expect(authenticatorWithHttpOnly._jwtVerifier.verify).toHaveBeenCalledTimes(2);
});

test('should set SameSite on cookies', async () => {
Expand All @@ -162,10 +168,12 @@ describe('private functions', () => {
authenticatorWithSameSite._jwtVerifier.cacheJwks(jwksData);

const username = 'toto';
const scope = 'phone email profile openid aws.cognito.signin.user.admin';
const domain = 'example.com';
const path = '/test';
jest.spyOn(authenticatorWithSameSite._jwtVerifier, 'verify');
authenticatorWithSameSite._jwtVerifier.verify.mockReturnValueOnce(Promise.resolve({ token_use: 'id', 'cognito:username': username }));
authenticatorWithSameSite._jwtVerifier.verify.mockReturnValueOnce(Promise.resolve({ token_use: 'access', 'scope': scope }));

const response = await authenticatorWithSameSite._getRedirectResponse({ accessToken: tokenData.access_token, idToken: tokenData.id_token, refreshToken: tokenData.refresh_token }, domain, path);
expect(response).toMatchObject({
Expand All @@ -184,7 +192,7 @@ describe('private functions', () => {
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.idToken=${tokenData.id_token}; Domain=${domain}; Expires=${DATE.toUTCString()}; Secure; HttpOnly; SameSite=Strict`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.LastAuthUser=${username}; Domain=${domain}; Expires=${DATE.toUTCString()}; Secure; HttpOnly; SameSite=Strict`},
]));
expect(authenticatorWithSameSite._jwtVerifier.verify).toHaveBeenCalled();
expect(authenticatorWithSameSite._jwtVerifier.verify).toHaveBeenCalledTimes(2);
});

test('should set Path on cookies', async () => {
Expand All @@ -201,10 +209,12 @@ describe('private functions', () => {
authenticatorWithPath._jwtVerifier.cacheJwks(jwksData);

const username = 'toto';
const scope = 'phone email profile openid aws.cognito.signin.user.admin';
const domain = 'example.com';
const path = '/test';
jest.spyOn(authenticatorWithPath._jwtVerifier, 'verify');
authenticatorWithPath._jwtVerifier.verify.mockReturnValueOnce(Promise.resolve({ token_use: 'id', 'cognito:username': username }));
authenticatorWithPath._jwtVerifier.verify.mockReturnValueOnce(Promise.resolve({ token_use: 'access', 'scope': scope }));

const response = await authenticatorWithPath._getRedirectResponse({ accessToken: tokenData.access_token, idToken: tokenData.id_token, refreshToken: tokenData.refresh_token }, domain, path);
expect(response).toMatchObject({
Expand All @@ -223,7 +233,7 @@ describe('private functions', () => {
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.idToken=${tokenData.id_token}; Domain=${domain}; Path=${cookiePath}; Expires=${DATE.toUTCString()}; Secure`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.LastAuthUser=${username}; Domain=${domain}; Path=${cookiePath}; Expires=${DATE.toUTCString()}; Secure`},
]));
expect(authenticatorWithPath._jwtVerifier.verify).toHaveBeenCalled();
expect(authenticatorWithPath._jwtVerifier.verify).toHaveBeenCalledTimes(2);
});

test('should set csrf tokens when the feature is enabled', async () => {
Expand All @@ -243,10 +253,12 @@ describe('private functions', () => {
authenticatorWithPath._jwtVerifier.cacheJwks(jwksData);

const username = 'toto';
const scope = 'phone email profile openid aws.cognito.signin.user.admin';
const domain = 'example.com';
const path = '/test';
jest.spyOn(authenticatorWithPath._jwtVerifier, 'verify');
authenticatorWithPath._jwtVerifier.verify.mockReturnValueOnce(Promise.resolve({ token_use: 'id', 'cognito:username': username }));
authenticatorWithPath._jwtVerifier.verify.mockReturnValueOnce(Promise.resolve({ token_use: 'access', 'scope': scope }));

const response = await authenticatorWithPath._getRedirectResponse({ accessToken: tokenData.access_token, idToken: tokenData.id_token, refreshToken: tokenData.refresh_token }, domain, path);
expect(response).toMatchObject({
Expand All @@ -268,7 +280,7 @@ describe('private functions', () => {
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${NONCE_COOKIE_NAME_SUFFIX}=; Path=${cookiePath}; Expires=${DATE.toUTCString()}; Secure`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${NONCE_HMAC_COOKIE_NAME_SUFFIX}=; Path=${cookiePath}; Expires=${DATE.toUTCString()}; Secure`},
]));
expect(authenticatorWithPath._jwtVerifier.verify).toHaveBeenCalled();
expect(authenticatorWithPath._jwtVerifier.verify).toHaveBeenCalledTimes(2);
});

test('should use overriden cookie settings', async () => {
Expand Down Expand Up @@ -297,10 +309,12 @@ describe('private functions', () => {
authenticatorWithPath._jwtVerifier.cacheJwks(jwksData);

const username = 'toto';
const scope = 'phone email profile openid aws.cognito.signin.user.admin';
const domain = 'example.com';
const path = '/test';
jest.spyOn(authenticatorWithPath._jwtVerifier, 'verify');
authenticatorWithPath._jwtVerifier.verify.mockReturnValueOnce(Promise.resolve({ token_use: 'id', 'cognito:username': username }));
authenticatorWithPath._jwtVerifier.verify.mockReturnValueOnce(Promise.resolve({ token_use: 'access', 'scope': scope }));

const response = await authenticatorWithPath._getRedirectResponse({ accessToken: tokenData.access_token, idToken: tokenData.id_token, refreshToken: tokenData.refresh_token }, domain, path);
expect(response).toMatchObject({
Expand All @@ -322,7 +336,7 @@ describe('private functions', () => {
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${NONCE_COOKIE_NAME_SUFFIX}=; Path=${cookiePath}; Expires=${DATE.toUTCString()}; Secure; HttpOnly`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${NONCE_HMAC_COOKIE_NAME_SUFFIX}=; Path=${cookiePath}; Expires=${DATE.toUTCString()}; Secure; HttpOnly`},
]));
expect(authenticatorWithPath._jwtVerifier.verify).toHaveBeenCalled();
expect(authenticatorWithPath._jwtVerifier.verify).toHaveBeenCalledTimes(2);
});

test('should getIdTokenFromCookie', () => {
Expand Down Expand Up @@ -1053,6 +1067,7 @@ describe('handleRefreshToken', () => {

test('should refresh tokens successfully', async () => {
const username = 'toto';
const scope = 'phone email profile openid aws.cognito.signin.user.admin';
authenticator._getTokensFromCookie.mockReturnValueOnce({ refreshToken: tokenData.refresh_token });
authenticator._jwtVerifier.verify.mockReturnValueOnce(Promise.resolve({ token_use: 'id', 'cognito:username': username }));
authenticator._fetchTokensFromRefreshToken.mockReturnValueOnce(Promise.resolve({
Expand Down
17 changes: 9 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ export class Authenticator {
this._jwtVerifier = CognitoJwtVerifier.create({
userPoolId: params.userPoolId,
clientId: params.userPoolAppId,
tokenUse: 'id',
});
this._csrfProtection = params.csrfProtection;
this._logoutConfiguration = params.logoutConfiguration;
Expand Down Expand Up @@ -276,8 +275,10 @@ export class Authenticator {
* @return Lambda@Edge response.
*/
async _getRedirectResponse(tokens: Tokens, domain: string, location: string): Promise<CloudFrontResultResponse> {
const decoded = await this._jwtVerifier.verify(tokens.idToken as string);
const username = decoded['cognito:username'] as string;
const decodedIdToken = await this._jwtVerifier.verify(tokens.idToken as string, { tokenUse: 'id' });
const decodedAccessToken = await this._jwtVerifier.verify(tokens.accessToken as string, {tokenUse: 'access' });
const username = decodedIdToken['cognito:username'] as string;
const scopes = decodedAccessToken['scope'] as string;
const usernameBase = `${this._cookieBase}.${username}`;
const cookieDomain = getCookieDomain(domain, this._disableCookieDomain, this._cookieDomain);
const cookieAttributes: CookieAttributes = {
Expand All @@ -292,7 +293,7 @@ export class Authenticator {
Cookies.serialize(`${usernameBase}.accessToken`, tokens.accessToken as string, this._getOverridenCookieAttributes(cookieAttributes, 'accessToken')),
Cookies.serialize(`${usernameBase}.idToken`, tokens.idToken as string, this._getOverridenCookieAttributes(cookieAttributes, 'idToken')),
...(tokens.refreshToken ? [Cookies.serialize(`${usernameBase}.refreshToken`, tokens.refreshToken, this._getOverridenCookieAttributes(cookieAttributes, 'refreshToken'))] : []),
Cookies.serialize(`${usernameBase}.tokenScopesString`, 'phone email profile openid aws.cognito.signin.user.admin', cookieAttributes),
Cookies.serialize(`${usernameBase}.tokenScopesString`, scopes, cookieAttributes),
Cookies.serialize(`${this._cookieBase}.LastAuthUser`, username, cookieAttributes),
];

Expand Down Expand Up @@ -468,7 +469,7 @@ export class Authenticator {

let responseCookies: string[] = [];
try {
const decoded = await this._jwtVerifier.verify(tokens.idToken as string);
const decoded = await this._jwtVerifier.verify(tokens.idToken as string, { tokenUse: 'id' });
const username = decoded['cognito:username'] as string;
this._logger.info({ msg: 'Token verified. Clearing cookies...', idToken: tokens.idToken, username });

Expand Down Expand Up @@ -609,7 +610,7 @@ export class Authenticator {
}
try {
this._logger.debug({ msg: 'Verifying token...', tokens });
const user = await this._jwtVerifier.verify(tokens.idToken as string);
const user = await this._jwtVerifier.verify(tokens.idToken as string, { tokenUse: 'id' });
this._logger.info({ msg: 'Forwarding request', path: request.uri, user });
return request;
} catch (err) {
Expand Down Expand Up @@ -659,7 +660,7 @@ export class Authenticator {
const tokens = this._getTokensFromCookie(request.headers.cookie);

this._logger.debug({ msg: 'Verifying token...', tokens });
const user = await this._jwtVerifier.verify(tokens.idToken as string);
const user = await this._jwtVerifier.verify(tokens.idToken as string, { tokenUse: 'id' });

this._logger.info({ msg: 'Redirecting user to', path: redirectURI, user });
return {
Expand Down Expand Up @@ -744,7 +745,7 @@ export class Authenticator {
let tokens = this._getTokensFromCookie(request.headers.cookie);

this._logger.debug({ msg: 'Verifying token...', tokens });
const user = await this._jwtVerifier.verify(tokens.idToken as string);
const user = await this._jwtVerifier.verify(tokens.idToken as string, { tokenUse: 'id' });

this._logger.debug({ msg: 'Refreshing tokens...', tokens, user });
tokens = await this._fetchTokensFromRefreshToken(redirectURI, tokens.refreshToken as string);
Expand Down