From 3a488bb7b9cf6439beb7fa91580d64855cbdcf15 Mon Sep 17 00:00:00 2001 From: George Lee Date: Fri, 17 Dec 2021 14:52:01 -1000 Subject: [PATCH] Add isAuthenticated to check if user is authenticated Implementation largely borrows from the `handle` function, but simply returns true or false instead of doing the work to redirect the user to the appropriate destination. --- __tests__/index.test.js | 76 ++++++++++++++++++++++++++++++----------- index.js | 41 +++++++++++++++++----- 2 files changed, 89 insertions(+), 28 deletions(-) diff --git a/__tests__/index.test.js b/__tests__/index.test.js index 461a6d5..b6cbc06 100644 --- a/__tests__/index.test.js +++ b/__tests__/index.test.js @@ -59,11 +59,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}; Secure`}, - {key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.refreshToken=${tokenData.refresh_token}; Domain=${domain}; Expires=${DATE}; Secure`}, - {key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.tokenScopesString=phone email profile openid aws.cognito.signin.user.admin; Domain=${domain}; Expires=${DATE}; Secure`}, - {key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.idToken=${tokenData.id_token}; Domain=${domain}; Expires=${DATE}; Secure`}, - {key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.LastAuthUser=${username}; Domain=${domain}; Expires=${DATE}; Secure`}, + { key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.accessToken=${tokenData.access_token}; Domain=${domain}; Expires=${DATE}; Secure` }, + { key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.refreshToken=${tokenData.refresh_token}; Domain=${domain}; Expires=${DATE}; Secure` }, + { key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.tokenScopesString=phone email profile openid aws.cognito.signin.user.admin; Domain=${domain}; Expires=${DATE}; Secure` }, + { key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.idToken=${tokenData.id_token}; Domain=${domain}; Expires=${DATE}; Secure` }, + { key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.LastAuthUser=${username}; Domain=${domain}; Expires=${DATE}; Secure` }, ])); expect(authenticator._jwtVerifier.verify).toHaveBeenCalled(); }); @@ -79,13 +79,13 @@ describe('private functions', () => { logLevel: 'error', }); authenticatorWithNoCookieDomain._jwtVerifier.cacheJwks(jwksData); - + const username = 'toto'; const domain = 'example.com'; const path = '/test'; 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); expect(response).toMatchObject({ status: '302', @@ -97,11 +97,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}; Secure`}, - {key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.refreshToken=${tokenData.refresh_token}; Expires=${DATE}; Secure`}, - {key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.tokenScopesString=phone email profile openid aws.cognito.signin.user.admin; Expires=${DATE}; Secure`}, - {key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.idToken=${tokenData.id_token}; Expires=${DATE}; Secure`}, - {key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.LastAuthUser=${username}; Expires=${DATE}; Secure`}, + { key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.accessToken=${tokenData.access_token}; Expires=${DATE}; Secure` }, + { key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.refreshToken=${tokenData.refresh_token}; Expires=${DATE}; Secure` }, + { key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.tokenScopesString=phone email profile openid aws.cognito.signin.user.admin; Expires=${DATE}; Secure` }, + { key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.${username}.idToken=${tokenData.id_token}; Expires=${DATE}; Secure` }, + { key: 'Set-Cookie', value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.LastAuthUser=${username}; Expires=${DATE}; Secure` }, ])); expect(authenticatorWithNoCookieDomain._jwtVerifier.verify).toHaveBeenCalled(); }); @@ -236,7 +236,7 @@ describe('handle', () => { }); test('should fetch and set token if code is present', () => { - authenticator._jwtVerifier.verify.mockImplementationOnce(async () => { throw new Error();}); + authenticator._jwtVerifier.verify.mockImplementationOnce(async () => { throw new Error(); }); authenticator._fetchTokensFromCode.mockResolvedValueOnce(tokenData); authenticator._getRedirectResponse.mockReturnValueOnce({ response: 'toto' }); const request = getCloudfrontRequest(); @@ -250,7 +250,7 @@ describe('handle', () => { }); test('should redirect to auth domain if unauthenticated and no code', () => { - authenticator._jwtVerifier.verify.mockImplementationOnce(async () => { throw new Error();}); + authenticator._jwtVerifier.verify.mockImplementationOnce(async () => { throw new Error(); }); return expect(authenticator.handle(getCloudfrontRequest())).resolves.toEqual( { status: 302, @@ -268,6 +268,44 @@ describe('handle', () => { }); }); +describe('isAuthenticated', () => { + let authenticator; + + beforeEach(() => { + authenticator = 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, + logLevel: 'debug', + }); + authenticator._jwtVerifier.cacheJwks(jwksData); + jest.spyOn(authenticator, '_getIdTokenFromCookie'); + jest.spyOn(authenticator, '_fetchTokensFromCode'); + jest.spyOn(authenticator, '_getRedirectResponse'); + jest.spyOn(authenticator._jwtVerifier, 'verify'); + }); + + test('return true if the user is authenticated', () => { + authenticator._jwtVerifier.verify.mockReturnValueOnce(Promise.resolve({})); + return expect(authenticator.isAuthenticated(getCloudfrontRequest())).resolves.toEqual(true) + .then(() => { + expect(authenticator._getIdTokenFromCookie).toHaveBeenCalled(); + expect(authenticator._jwtVerifier.verify).toHaveBeenCalled(); + }); + }); + + test('return false if the user is not authenticated', () => { + authenticator._jwtVerifier.verify.mockImplementationOnce(async () => { throw new Error(); }); + return expect(authenticator.isAuthenticated(getCloudfrontRequest())).resolves.toEqual(false) + .then(() => { + expect(authenticator._jwtVerifier.verify).toHaveBeenCalled(); + }); + }); +}); + + /* eslint-disable quotes, comma-dangle */ const jwksData = { @@ -278,11 +316,11 @@ const jwksData = { }; const tokenData = { - "access_token":"eyJz9sdfsdfsdfsd", - "refresh_token":"dn43ud8uj32nk2je", - "id_token":"dmcxd329ujdmkemkd349r", - "token_type":"Bearer", - 'expires_in':3600, + "access_token": "eyJz9sdfsdfsdfsd", + "refresh_token": "dn43ud8uj32nk2je", + "id_token": "dmcxd329ujdmkemkd349r", + "token_type": "Bearer", + 'expires_in': 3600, }; const getCloudfrontRequest = () => ({ diff --git a/index.js b/index.js index a1d7938..08c6094 100644 --- a/index.js +++ b/index.js @@ -34,7 +34,7 @@ class Authenticator { if (typeof params !== 'object') { throw new Error('Expected params to be an object'); } - [ 'region', 'userPoolId', 'userPoolAppId', 'userPoolDomain' ].forEach(param => { + ['region', 'userPoolId', 'userPoolAppId', 'userPoolDomain'].forEach(param => { if (typeof params[param] !== 'string') { throw new Error(`Expected params.${param} to be a string`); } @@ -60,13 +60,13 @@ class Authenticator { method: 'post', headers: { 'Content-Type': 'application/x-www-form-urlencoded', - ...(authorization && {'Authorization': `Basic ${authorization}`}), + ...(authorization && { 'Authorization': `Basic ${authorization}` }), }, data: querystring.stringify({ - client_id: this._userPoolAppId, - code: code, - grant_type: 'authorization_code', - redirect_uri: redirectURI, + client_id: this._userPoolAppId, + code: code, + grant_type: 'authorization_code', + redirect_uri: redirectURI, }), }; this._logger.debug({ msg: 'Fetching tokens from grant code...', request, code }); @@ -92,11 +92,11 @@ 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(new Date() * 1 + this._cookieExpirationDays * 864e+5)}; Secure` : + const directives = (!this._disableCookieDomain) ? + `Domain=${domain}; Expires=${new Date(new Date() * 1 + this._cookieExpirationDays * 864e+5)}; Secure` : `Expires=${new Date(new Date() * 1 + this._cookieExpirationDays * 864e+5)}; Secure`; const response = { - status: '302' , + status: '302', headers: { 'location': [{ key: 'Location', 'value': location }], 'set-cookie': [ @@ -197,6 +197,29 @@ class Authenticator { } } } + + /** + * Check if user is authenticated: + * * if authentication cookie is present and valid: return true + * * else return false + * @param {Object} event Lambda@Edge event. + * @return {Boolean} True if user is authenticated. + */ + async isAuthenticated(event) { + this._logger.debug({ msg: 'Checking if Lambda@Edge event is authenticated', event }); + + const { request } = event.Records[0].cf; + + try { + const token = this._getIdTokenFromCookie(request.headers.cookie); + this._logger.debug({ msg: 'Verifying token...', token }); + await this._jwtVerifier.verify(token); + + return true; + } catch (err) { + return false; + } + } } module.exports.Authenticator = Authenticator;