Skip to content

Commit

Permalink
Add support for HttpOnly cookie flag (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
tsop14 authored Nov 14, 2022
1 parent ff063fe commit 096f3ee
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ For an explanation of the interactions between CloudFront, Cognito and Lambda@Ed
* `userPoolDomain` *string* Cognito UserPool domain (eg: `your-domain.auth.us-east-1.amazoncognito.com`)
* `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.
* `logLevel` *string* (Optional) Logging level. Default: `'silent'`. One of `'fatal'`, `'error'`, `'warn'`, `'info'`, `'debug'`, `'trace'` or `'silent'`.

*This is the class constructor.*
Expand Down
51 changes: 51 additions & 0 deletions __tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ describe('private functions', () => {
userPoolDomain: 'my-cognito-domain.auth.us-east-1.amazoncognito.com',
cookieExpirationDays: 365,
disableCookieDomain: false,
httpOnly: false,
logLevel: 'error',
});
});
Expand Down Expand Up @@ -107,6 +108,45 @@ describe('private functions', () => {
expect(authenticatorWithNoCookieDomain._jwtVerifier.verify).toHaveBeenCalled();
});

test('should set HttpOnly on cookies', async () => {
const authenticatorWithHttpOnly = new Authenticator({
region: 'us-east-1',
userPoolId: 'us-east-1_abcdef123',
userPoolAppId: '123456789qwertyuiop987abcd',
userPoolDomain: 'my-cognito-domain.auth.us-east-1.amazoncognito.com',
cookieExpirationDays: 365,
disableCookieDomain: false,
httpOnly: true,
logLevel: 'error',
});
authenticatorWithHttpOnly._jwtVerifier.cacheJwks(jwksData);

const username = 'toto';
const domain = 'example.com';
const path = '/test';
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);
expect(response).toMatchObject({
status: '302',
headers: {
location: [{
key: 'Location',
value: path,
}],
},
});
expect(response.headers['set-cookie']).toEqual(expect.arrayContaining([
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.accessToken=${tokenData.access_token}; Domain=${domain}; Expires=${DATE}; Secure; HttpOnly`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.refreshToken=${tokenData.refresh_token}; Domain=${domain}; Expires=${DATE}; Secure; HttpOnly`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.tokenScopesString=phone email profile openid aws.cognito.signin.user.admin; Domain=${domain}; Expires=${DATE}; Secure; HttpOnly`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.idToken=${tokenData.id_token}; Domain=${domain}; Expires=${DATE}; Secure; HttpOnly`},
{key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.LastAuthUser=${username}; Domain=${domain}; Expires=${DATE}; Secure; HttpOnly`},
]));
expect(authenticatorWithHttpOnly._jwtVerifier.verify).toHaveBeenCalled();
});

test('should getIdTokenFromCookie', () => {
const appClientName = 'toto,./;;..-_lol123';
expect(
Expand Down Expand Up @@ -142,6 +182,7 @@ describe('createAuthenticator', () => {
userPoolDomain: 'my-cognito-domain.auth.us-east-1.amazoncognito.com',
cookieExpirationDays: 365,
disableCookieDomain: true,
httpOnly: false,
};
});

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

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

test('should fail when creating authenticator without params', () => {
// @ts-ignore
Expand Down Expand Up @@ -215,6 +261,11 @@ describe('createAuthenticator', () => {
params.disableCookieDomain = '123';
expect(() => new Authenticator(params)).toThrow('disableCookieDomain');
});

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

describe('handle', () => {
Expand Down
16 changes: 13 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface AuthenticatorParams {
userPoolDomain: string;
cookieExpirationDays?: number;
disableCookieDomain?: boolean;
httpOnly?: boolean;
logLevel?: 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'silent';
}

Expand All @@ -23,6 +24,7 @@ export class Authenticator {
_userPoolDomain: string;
_cookieExpirationDays: number;
_disableCookieDomain: boolean;
_httpOnly: boolean;
_cookieBase: string;
_logger;
_jwtVerifier;
Expand All @@ -36,6 +38,7 @@ export class Authenticator {
this._userPoolDomain = params.userPoolDomain;
this._cookieExpirationDays = params.cookieExpirationDays || 365;
this._disableCookieDomain = ('disableCookieDomain' in params && params.disableCookieDomain === true) ? true : false;
this._httpOnly = ('httpOnly' in params && params.httpOnly === true) ? true : false;
this._cookieBase = `CognitoIdentityServiceProvider.${params.userPoolAppId}`;
this._logger = pino({
level: params.logLevel || 'silent', // Default to silent
Expand Down Expand Up @@ -68,6 +71,9 @@ export class Authenticator {
if ('disableCookieDomain' in params && typeof params.disableCookieDomain !== 'boolean') {
throw new Error('Expected params.disableCookieDomain to be a boolean');
}
if ('httpOnly' in params && typeof params.httpOnly !== 'boolean') {
throw new Error('Expected params.httpOnly to be a boolean');
}
}

/**
Expand Down Expand Up @@ -115,9 +121,13 @@ export class Authenticator {
const decoded = await this._jwtVerifier.verify(tokens.id_token);
const username = decoded['cognito:username'];
const usernameBase = `${this._cookieBase}.${username}`;
const directives = (!this._disableCookieDomain) ?
`Domain=${domain}; Expires=${new Date(Date.now() + this._cookieExpirationDays * 864e+5)}; Secure` :
`Expires=${new Date(Date.now() + this._cookieExpirationDays * 864e+5)}; Secure`;
const directives = [
...((this._disableCookieDomain) ? [] : [`Domain=${domain}`]),
`Expires=${new Date(Date.now() + this._cookieExpirationDays * 864e+5)}`,
'Secure',
...((this._httpOnly) ? ['HttpOnly'] : []),
].join('; ');

const response = {
status: '302' ,
headers: {
Expand Down

0 comments on commit 096f3ee

Please sign in to comment.