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

Added logout functionality #60

Closed
wants to merge 2 commits into from
Closed
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ For an explanation of the interactions between CloudFront, Cognito and Lambda@Ed
* `cookieExpirationDays` *number* (Optional) Number of day to set cookies expiration date, default to 365 days (eg: `365`)
* `disableCookieDomain` *boolean* (Optional) Sets domain attribute in cookies, defaults to false (eg: `false`)
* `httpOnly` *boolean* (Optional) Forbids JavaScript from accessing the cookies, defaults to false (eg: `false`). Note, if this is set to `true`, the cookies will not be accessible to Amplify auth if you are using it client side.
* `enableLogout` *boolean* (Optional) Enables logout flow on `/logout` URI.
* `sameSite` *Strict | Lax | None* (Optional) Allows you to declare if your cookie should be restricted to a first-party or same-site context (eg: `SameSite=None`).
* `logLevel` *string* (Optional) Logging level. Default: `'silent'`. One of `'fatal'`, `'error'`, `'warn'`, `'info'`, `'debug'`, `'trace'` or `'silent'`.

Expand Down
155 changes: 120 additions & 35 deletions __tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import axios from 'axios';

jest.mock('axios');
jest.useFakeTimers();
jest.setSystemTime(new Date('2017-01-01'))

import { Authenticator } from '../src/';
import { Cookies } from '../src/util/cookie';

const DATE = new Date('2017');
// @ts-ignore
global.Date = class extends Date {
constructor() {
super();
return DATE;
}
};
const DATE = Date.now();
const EPOCH_ZERO_DATE = new Date(0);

describe('private functions', () => {
let authenticator;
Expand Down Expand Up @@ -48,10 +44,12 @@ describe('private functions', () => {
const username = 'toto';
const domain = 'example.com';
const path = '/test';
const expire = false;
const expirationDate = new Date(DATE.valueOf() + authenticator._cookieExpirationDays * 864e+5).toUTCString();
jest.spyOn(authenticator._jwtVerifier, 'verify');
authenticator._jwtVerifier.verify.mockReturnValueOnce(Promise.resolve({ token_use: 'id', 'cognito:username': username }));

const response = await authenticator._getRedirectResponse(tokenData, domain, path);
const response = await authenticator._getRedirectResponse(tokenData, domain, path, expire);
expect(response).toMatchObject({
status: '302',
headers: {
Expand All @@ -62,11 +60,11 @@ describe('private functions', () => {
},
});
expect(response.headers['set-cookie']).toEqual(expect.arrayContaining([
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.accessToken=${tokenData.access_token}; Domain=${domain}; Expires=${DATE.toUTCString()}; Secure`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.refreshToken=${tokenData.refresh_token}; Domain=${domain}; Expires=${DATE.toUTCString()}; Secure`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.tokenScopesString=phone%20email%20profile%20openid%20aws.cognito.signin.user.admin; Domain=${domain}; Expires=${DATE.toUTCString()}; Secure`},
{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`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.accessToken=${tokenData.access_token}; Domain=${domain}; Expires=${expirationDate}; Secure`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.refreshToken=${tokenData.refresh_token}; Domain=${domain}; Expires=${expirationDate}; Secure`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.tokenScopesString=phone%20email%20profile%20openid%20aws.cognito.signin.user.admin; Domain=${domain}; Expires=${expirationDate}; Secure`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.idToken=${tokenData.id_token}; Domain=${domain}; Expires=${expirationDate}; Secure`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.LastAuthUser=${username}; Domain=${domain}; Expires=${expirationDate}; Secure`},
]));
expect(authenticator._jwtVerifier.verify).toHaveBeenCalled();
});
Expand All @@ -86,10 +84,12 @@ describe('private functions', () => {
const username = 'toto';
const domain = 'example.com';
const path = '/test';
const expire = false;
const expirationDate = new Date(DATE.valueOf() + authenticator._cookieExpirationDays * 864e+5).toUTCString();
jest.spyOn(authenticatorWithNoCookieDomain._jwtVerifier, 'verify');
authenticatorWithNoCookieDomain._jwtVerifier.verify.mockReturnValueOnce(Promise.resolve({ token_use: 'id', 'cognito:username': username }));

const response = await authenticatorWithNoCookieDomain._getRedirectResponse(tokenData, domain, path);
const response = await authenticatorWithNoCookieDomain._getRedirectResponse(tokenData, domain, path, expire);
expect(response).toMatchObject({
status: '302',
headers: {
Expand All @@ -100,11 +100,11 @@ describe('private functions', () => {
},
});
expect(response.headers['set-cookie']).toEqual(expect.arrayContaining([
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.accessToken=${tokenData.access_token}; Expires=${DATE.toUTCString()}; Secure`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.refreshToken=${tokenData.refresh_token}; Expires=${DATE.toUTCString()}; Secure`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.tokenScopesString=phone%20email%20profile%20openid%20aws.cognito.signin.user.admin; Expires=${DATE.toUTCString()}; Secure`},
{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`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.accessToken=${tokenData.access_token}; Expires=${expirationDate}; Secure`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.refreshToken=${tokenData.refresh_token}; Expires=${expirationDate}; Secure`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.tokenScopesString=phone%20email%20profile%20openid%20aws.cognito.signin.user.admin; Expires=${expirationDate}; Secure`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.idToken=${tokenData.id_token}; Expires=${expirationDate}; Secure`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.LastAuthUser=${username}; Expires=${expirationDate}; Secure`},
]));
expect(authenticatorWithNoCookieDomain._jwtVerifier.verify).toHaveBeenCalled();
});
Expand All @@ -125,10 +125,12 @@ describe('private functions', () => {
const username = 'toto';
const domain = 'example.com';
const path = '/test';
const expire = false;
const expirationDate = new Date(DATE.valueOf() + authenticator._cookieExpirationDays * 864e+5).toUTCString();
jest.spyOn(authenticatorWithHttpOnly._jwtVerifier, 'verify');
authenticatorWithHttpOnly._jwtVerifier.verify.mockReturnValueOnce(Promise.resolve({ token_use: 'id', 'cognito:username': username }));

const response = await authenticatorWithHttpOnly._getRedirectResponse(tokenData, domain, path);
const response = await authenticatorWithHttpOnly._getRedirectResponse(tokenData, domain, path, expire);
expect(response).toMatchObject({
status: '302',
headers: {
Expand All @@ -139,11 +141,11 @@ describe('private functions', () => {
},
});
expect(response.headers['set-cookie']).toEqual(expect.arrayContaining([
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.accessToken=${tokenData.access_token}; Domain=${domain}; Expires=${DATE.toUTCString()}; Secure; HttpOnly`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.refreshToken=${tokenData.refresh_token}; Domain=${domain}; Expires=${DATE.toUTCString()}; Secure; HttpOnly`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.tokenScopesString=phone%20email%20profile%20openid%20aws.cognito.signin.user.admin; Domain=${domain}; Expires=${DATE.toUTCString()}; Secure; HttpOnly`},
{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`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.accessToken=${tokenData.access_token}; Domain=${domain}; Expires=${expirationDate}; Secure; HttpOnly`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.refreshToken=${tokenData.refresh_token}; Domain=${domain}; Expires=${expirationDate}; Secure; HttpOnly`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.tokenScopesString=phone%20email%20profile%20openid%20aws.cognito.signin.user.admin; Domain=${domain}; Expires=${expirationDate}; Secure; HttpOnly`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.idToken=${tokenData.id_token}; Domain=${domain}; Expires=${expirationDate}; Secure; HttpOnly`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.LastAuthUser=${username}; Domain=${domain}; Expires=${expirationDate}; Secure; HttpOnly`},
]));
expect(authenticatorWithHttpOnly._jwtVerifier.verify).toHaveBeenCalled();
});
Expand All @@ -165,10 +167,12 @@ describe('private functions', () => {
const username = 'toto';
const domain = 'example.com';
const path = '/test';
const expire = false;
const expirationDate = new Date(DATE.valueOf() + authenticator._cookieExpirationDays * 864e+5).toUTCString();
jest.spyOn(authenticatorWithSameSite._jwtVerifier, 'verify');
authenticatorWithSameSite._jwtVerifier.verify.mockReturnValueOnce(Promise.resolve({ token_use: 'id', 'cognito:username': username }));

const response = await authenticatorWithSameSite._getRedirectResponse(tokenData, domain, path);
const response = await authenticatorWithSameSite._getRedirectResponse(tokenData, domain, path, expire);
expect(response).toMatchObject({
status: '302',
headers: {
Expand All @@ -179,11 +183,11 @@ describe('private functions', () => {
},
});
expect(response.headers['set-cookie']).toEqual(expect.arrayContaining([
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.accessToken=${tokenData.access_token}; Domain=${domain}; Expires=${DATE.toUTCString()}; Secure; HttpOnly; SameSite=Strict`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.refreshToken=${tokenData.refresh_token}; Domain=${domain}; Expires=${DATE.toUTCString()}; Secure; HttpOnly; SameSite=Strict`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.tokenScopesString=phone%20email%20profile%20openid%20aws.cognito.signin.user.admin; Domain=${domain}; Expires=${DATE.toUTCString()}; Secure; HttpOnly; SameSite=Strict`},
{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`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.accessToken=${tokenData.access_token}; Domain=${domain}; Expires=${expirationDate}; Secure; HttpOnly; SameSite=Strict`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.refreshToken=${tokenData.refresh_token}; Domain=${domain}; Expires=${expirationDate}; Secure; HttpOnly; SameSite=Strict`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.tokenScopesString=phone%20email%20profile%20openid%20aws.cognito.signin.user.admin; Domain=${domain}; Expires=${expirationDate}; Secure; HttpOnly; SameSite=Strict`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.idToken=${tokenData.id_token}; Domain=${domain}; Expires=${expirationDate}; Secure; HttpOnly; SameSite=Strict`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.LastAuthUser=${username}; Domain=${domain}; Expires=${expirationDate}; Secure; HttpOnly; SameSite=Strict`},
]));
expect(authenticatorWithSameSite._jwtVerifier.verify).toHaveBeenCalled();
});
Expand Down Expand Up @@ -234,6 +238,7 @@ describe('createAuthenticator', () => {
cookieExpirationDays: 365,
disableCookieDomain: true,
httpOnly: false,
enableLogout: true
};
});

Expand All @@ -256,6 +261,11 @@ describe('createAuthenticator', () => {
expect(typeof new Authenticator(params)).toBe('object');
});

test('should create authenticator without enableLogout', () => {
delete params.enableLogout;
expect(typeof new Authenticator(params)).toBe('object');
});

test('should create authenticator with unvalidated samesite', () => {
params.sameSite = '123';
expect(() => new Authenticator(params)).toThrow('Expected params');
Expand Down Expand Up @@ -322,6 +332,11 @@ describe('createAuthenticator', () => {
params.httpOnly = '123';
expect(() => new Authenticator(params)).toThrow('httpOnly');
});

test('should fail when creating authenticator with invalid enableLogout', () => {
params.enableLogout = '123';
expect(() => new Authenticator(params)).toThrow('enableLogout');
});
});

describe('handle', () => {
Expand All @@ -334,6 +349,7 @@ describe('handle', () => {
userPoolAppId: '123456789qwertyuiop987abcd',
userPoolDomain: 'my-cognito-domain.auth.us-east-1.amazoncognito.com',
cookieExpirationDays: 365,
enableLogout: true,
logLevel: 'debug',
});
authenticator._jwtVerifier.cacheJwks(jwksData);
Expand All @@ -352,6 +368,74 @@ describe('handle', () => {
});
});

test('should redirect to logout if /logout is called', () => {
const username = 'toto';
const request = getCloudfrontRequest();
request.Records[0].cf.request.uri = '/logout';
const domain = request.Records[0].cf.request.headers.host[0].value;

authenticator._jwtVerifier.verify.mockReturnValueOnce({'cognito:username':'toto'});
return expect(authenticator.handle(request)).resolves.toEqual(
{
status: '302',
headers: {
'location': [{
key: 'Location',
value: 'https://my-cognito-domain.auth.us-east-1.amazoncognito.com/logout?logout_uri=https://d111111abcdef8.cloudfront.net&client_id=123456789qwertyuiop987abcd',
}],
'cache-control': [{
key: 'Cache-Control',
value: 'no-cache, no-store, max-age=0, must-revalidate',
}],
'pragma': [{
key: 'Pragma',
value: 'no-cache',
}],
'set-cookie': [
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.accessToken=0; Domain=${domain}; Expires=${EPOCH_ZERO_DATE.toUTCString()}; Secure`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.idToken=${tokenData.access_token}; Domain=${domain}; Expires=${EPOCH_ZERO_DATE.toUTCString()}; Secure`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.refreshToken=0; Domain=${domain}; Expires=${EPOCH_ZERO_DATE.toUTCString()}; Secure`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.tokenScopesString=phone%20email%20profile%20openid%20aws.cognito.signin.user.admin; Domain=${domain}; Expires=${EPOCH_ZERO_DATE.toUTCString()}; Secure`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.LastAuthUser=${username}; Domain=${domain}; Expires=${EPOCH_ZERO_DATE.toUTCString()}; Secure`},
]
},
}
)
.then(() => {
expect(authenticator._getIdTokenFromCookie).toHaveBeenCalled();
expect(authenticator._jwtVerifier.verify).toHaveBeenCalled();
});

});

test('should redirect to root of domain if called /logout without cookies', () =>{
const request = getCloudfrontRequest();
const domain = request.Records[0].cf.request.headers.host[0].value;
request.Records[0].cf.request.uri = '/logout';
request.Records[0].cf.request.headers.cookie = [];

return expect(authenticator.handle(request)).resolves.toEqual(
{
status: '302',
headers: {
'location': [{
key: 'Location',
value: domain,
}],
'cache-control': [{
key: 'Cache-Control',
value: 'no-cache, no-store, max-age=0, must-revalidate',
}],
'pragma': [{
key: 'Pragma',
value: 'no-cache',
}],
},
}

);
});

test('should fetch and set token if code is present', () => {
authenticator._jwtVerifier.verify.mockImplementationOnce(async () => { throw new Error();});
authenticator._fetchTokensFromCode.mockResolvedValueOnce(tokenData);
Expand All @@ -362,10 +446,11 @@ describe('handle', () => {
.then(() => {
expect(authenticator._jwtVerifier.verify).toHaveBeenCalled();
expect(authenticator._fetchTokensFromCode).toHaveBeenCalled();
expect(authenticator._getRedirectResponse).toHaveBeenCalledWith(tokenData, 'd111111abcdef8.cloudfront.net', '/lol');
expect(authenticator._getRedirectResponse).toHaveBeenCalledWith(tokenData, 'd111111abcdef8.cloudfront.net', '/lol', false);
});
});


test('should redirect to auth domain if unauthenticated and no code', () => {
authenticator._jwtVerifier.verify.mockImplementationOnce(async () => { throw new Error();});
return expect(authenticator.handle(getCloudfrontRequest())).resolves.toEqual(
Expand Down Expand Up @@ -491,4 +576,4 @@ const getCloudfrontRequest = () => ({
}
}
]
});
});
Loading