From 2256ec248bf7fcfd09ecba6aeb4083687bba9e52 Mon Sep 17 00:00:00 2001 From: Hugo David-Boyet Date: Mon, 11 Nov 2019 17:03:50 +0100 Subject: [PATCH] Add cognito-at-edge library --- .eslintrc.json | 21 +++ .gitignore | 3 + README.md | 70 +++++++- __tests__/index.test.js | 346 ++++++++++++++++++++++++++++++++++++++++ doc/architecture.png | Bin 0 -> 32245 bytes index.js | 227 ++++++++++++++++++++++++++ package.json | 41 +++++ 7 files changed, 701 insertions(+), 7 deletions(-) create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 __tests__/index.test.js create mode 100644 doc/architecture.png create mode 100644 index.js create mode 100644 package.json diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..b91696c --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "env": { + "es6": true, + "node": true, + "amd": true, + "jest": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 2018 + }, + "rules": { + "indent": ["error", 2], + "linebreak-style": ["error", "unix"], + "quotes": ["error", "single", { "avoidEscape": true }], + "camelcase": [2, { "properties": "never" }], + "semi": ["error", "always"], + "comma-dangle": ["error", "always-multiline"], + "no-console": "off" + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..200aa91 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +package-lock.json +output-template.yml diff --git a/README.md b/README.md index 8f46b7c..735343f 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,69 @@ -## My Project +## cognito-at-edge +*Serverless authentication solution to protect your website or Amplify application.* -TODO: Fill this README out! +![Architecture](./doc/architecture.png) +This NodeJS library authenticate CloudFront requests with Lambda@Edge based and a Cognito UserPool. -Be sure to: +### Requirements +* NodeJS v10+ (install with [NVM](https://github.com/nvm-sh/nvm)) +* aws-cli installed and configured ([installation guide](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html)) -* Change the title in this README -* Edit your repository description on GitHub +### Usage -## License +Install the `cognito-at-edge` package: +``` +npm install --save cognito-at-edge +``` -This project is licensed under the Apache-2.0 License. +Create the a Lambda@Edge function with the following content and modify the parameters based on your configuration: +``` +const { Authenticator } = require('cognito-at-edge'); + +const authenticator = new Authenticator({ + region: 'us-east-1', // user pool region + userPoolId: 'us-east-1_tyo1a1FHH', + userPoolAppId: '63gcbm2jmskokurt5ku9fhejc6', + userPoolDomain: 'domain.auth.us-east-1.amazoncognito.com', + logLevel: 'error', +}); + +exports.handler = async (request) => authenticator.handle(request); +``` + +**Every `request` will be authenticated by the `Authenticator.handle` function.** + +### Getting started + +Based on your requirements you can use of the solution below. They all provide the complete infrastructure leveraging `cognito-at-edge` to protect a website or an Amplify application. + +*WIP* + +### Reference +#### Authenticator Class +##### Authenticator(params) +* `params` *Object* Authenticator parameters: + * `region` *string* Cognito UserPool region (eg: `us-east-1`) + * `userPoolId` *string* Cognito UserPool ID (eg: `us-east-1_tyo1a1FHH`) + * `userPoolAppId` *string* Cognito UserPool Application ID (eg: `63gcbm2jmskokurt5ku9fhejc6`) + * `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`) + * `logLevel` *string* (Optional) Logging level. Default: `'silent'`. One of `'fatal'`, `'error'`, `'warn'`, `'info'`, `'debug'`, `'trace'` or `'silent'`. + +*This is the class constructor.* + +##### handle(request) +* `request` *Object* Lambda@Edge request Object + * cf AWS doc for details: [Lambda@Edge events](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html) + +Use it as your Lambda Handler. It will authenticate each query. +``` +const authenticator = new Authenticator( ... ); +exports.handler = async (request) => authenticator.handle(request); +``` + +### Contact +Please fill an issue in the Github repository ([Open issues](https://github.com/awslabs/cognito-at-edge/issues)). + +## License +This project is licensed under the Apache-2.0 License. diff --git a/__tests__/index.test.js b/__tests__/index.test.js new file mode 100644 index 0000000..d299f39 --- /dev/null +++ b/__tests__/index.test.js @@ -0,0 +1,346 @@ +const axios = require('axios'); +const jwt = require('jsonwebtoken'); + +jest.mock('axios'); +jest.mock('jwk-to-pem'); +jest.mock('jsonwebtoken'); + +const { Authenticator } = require('../index'); + +const DATE = new Date('2017'); +global.Date = class extends Date { + constructor() { + return DATE; + } +}; + +describe('private functions', () => { + 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: 'error', + }); + }); + + test('JWKS should be false by default', () => { + expect(authenticator._jwks).toBeFalsy(); + }); + + test('should fetch JWKS', () => { + axios.get.mockResolvedValue({ data: jwksData }); + return authenticator._fetchJWKS('http://something') + .then(() => { + expect(authenticator._jwks).toEqual({ + '1234example=': { 'kid': '1234example=', 'alg': 'RS256', 'kty': 'RSA', 'e': 'AQAB', 'n': '1234567890', 'use': 'sig' }, + '5678example=': { 'kid': '5678example=', 'alg': 'RS256', 'kty': 'RSA', 'e': 'AQAB', 'n': '987654321', 'use': 'sig' }, + }); + }); + }); + + test('should get valid decoded token', () => { + authenticator._jwks = {}; + jwt.decode.mockReturnValueOnce({ header: { kid: 'kid' } }); + jwt.verify.mockReturnValueOnce({ token_use: 'id', attribute: 'valid' }); + expect(authenticator._getVerifiedToken('valid-token')).toEqual({ token_use: 'id', attribute: 'valid' }); + }); + + test('should fetch token', () => { + axios.request.mockResolvedValue({ data: tokenData }); + + return authenticator._fetchTokensFromCode('htt://redirect', 'AUTH_CODE') + .then(res => { + expect(res).toEqual(tokenData); + }); + }); + + test('should getRedirectResponse', () => { + const username = 'toto'; + const domain = 'example.com'; + const path = '/test'; + jest.spyOn(authenticator, '_getVerifiedToken'); + authenticator._getVerifiedToken.mockReturnValueOnce({ token_use: 'id', 'cognito:username': username }); + + const response = authenticator._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`}, + {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._getVerifiedToken).toHaveBeenCalled(); + }); + + test('should getIdTokenFromCookie', () => { + expect( + authenticator._getIdTokenFromCookie([{ + key: 'Cookie', + value: `CognitoIdentityServiceProvider.5uka3k8840tap1g1i1617jh8pi.MyFederation_toto123.idToken=wrong; CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.MyFederation_toto123.idToken=${tokenData.id_token}; CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.MyFederation_toto123.idToken=${tokenData.id_token}; CognitoIdentityServiceProvider.5ukasw8840tap1g1i1617jh8pi.MyFederation_toto123.idToken=wrong;`, + }]), + ).toBe(tokenData.id_token); + }); + + test('should getIdTokenFromCookie throw on cookies', () => { + expect(() => authenticator._getIdTokenFromCookie()).toThrow('Id token'); + expect(() => authenticator._getIdTokenFromCookie('')).toThrow('Id token'); + expect(() => authenticator._getIdTokenFromCookie([])).toThrow('Id token'); + }); +}); + +describe('createAuthenticator', () => { + let params; + + beforeEach(() => { + params = { + region: 'us-east-1', + userPoolId: 'us-east-1_abcdef123', + userPoolAppId: '123456789qwertyuiop987abcd', + userPoolDomain: 'my-cognito-domain.auth.us-east-1.amazoncognito.com', + cookieExpirationDays: 365, + }; + }); + + test('should create authenticator', () => { + expect(typeof new Authenticator(params)).toBe('object'); + }); + + test('should create authenticator without cookieExpirationDay', () => { + delete params.cookieExpirationDays; + expect(typeof new Authenticator(params)).toBe('object'); + }); + + test('should fail when creating authenticator without params', () => { + expect(() => new Authenticator()).toThrow('Expected params'); + expect(() => new Authenticator()).toThrow('Expected params'); + }); + + test('should fail when creating authenticator without region', () => { + delete params.region; + expect(() => new Authenticator(params)).toThrow('region'); + }); + + test('should fail when creating authenticator without userPoolId', () => { + delete params.userPoolId; + expect(() => new Authenticator(params)).toThrow('userPoolId'); + }); + + test('should fail when creating authenticator without userPoolAppId', () => { + delete params.userPoolAppId; + expect(() => new Authenticator(params)).toThrow('userPoolAppId'); + }); + + test('should fail when creating authenticator without userPoolDomain', () => { + delete params.userPoolDomain; + expect(() => new Authenticator(params)).toThrow('userPoolDomain'); + }); + + test('should fail when creating authenticator with invalid region', () => { + params.region = 123; + expect(() => new Authenticator(params)).toThrow('region'); + }); + + test('should fail when creating authenticator with invalid userPoolId', () => { + params.userPoolId = 123; + expect(() => new Authenticator(params)).toThrow('userPoolId'); + }); + + test('should fail when creating authenticator with invalid userPoolAppId', () => { + params.userPoolAppId = 123; + expect(() => new Authenticator(params)).toThrow('userPoolAppId'); + }); + + test('should fail when creating authenticator with invalid userPoolDomain', () => { + params.userPoolDomain = 123; + expect(() => new Authenticator(params)).toThrow('userPoolDomain'); + }); + + test('should fail when creating authenticator with invalid cookieExpirationDay', () => { + params.cookieExpirationDays = '123'; + expect(() => new Authenticator(params)).toThrow('cookieExpirationDays'); + }); +}); + +describe('handle', () => { + 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._jwks = jwksData; + jest.spyOn(authenticator, '_fetchJWKS'); + jest.spyOn(authenticator, '_getVerifiedToken'); + jest.spyOn(authenticator, '_getIdTokenFromCookie'); + jest.spyOn(authenticator, '_fetchTokensFromCode'); + jest.spyOn(authenticator, '_getRedirectResponse'); + }); + + test('should fetch JWKS if not present', () => { + authenticator._jwks = undefined; + authenticator._fetchJWKS.mockResolvedValueOnce(jwksData); + return authenticator.handle(getCloudfrontRequest()) + .catch(err => err) + .finally(() => expect(authenticator._fetchJWKS).toHaveBeenCalled()); + }); + + test('should forward request if authenticated', () => { + authenticator._getVerifiedToken.mockReturnValueOnce({}); + return expect(authenticator.handle(getCloudfrontRequest())).resolves.toEqual(getCloudfrontRequest().Records[0].cf.request) + .then(() => { + expect(authenticator._getIdTokenFromCookie).toHaveBeenCalled(); + expect(authenticator._getVerifiedToken).toHaveBeenCalled(); + }); + }); + + test('should fetch and set token if code is present', () => { + authenticator._getVerifiedToken.mockImplementationOnce(() => { throw new Error();}); + authenticator._fetchTokensFromCode.mockResolvedValueOnce(tokenData); + authenticator._getRedirectResponse.mockReturnValueOnce({ response: 'toto' }); + const request = getCloudfrontRequest(); + request.Records[0].cf.request.querystring = 'code=54fe5f4e&state=/lol'; + return expect(authenticator.handle(request)).resolves.toEqual({ response: 'toto' }) + .then(() => { + expect(authenticator._getVerifiedToken).toHaveBeenCalled(); + expect(authenticator._fetchTokensFromCode).toHaveBeenCalled(); + expect(authenticator._getRedirectResponse).toHaveBeenCalledWith(tokenData, 'd111111abcdef8.cloudfront.net', '/lol'); + }); + }); + + test('should redirect to auth domain if unauthenticated and no code', () => { + authenticator._getVerifiedToken.mockImplementationOnce(() => { throw new Error();}); + return expect(authenticator.handle(getCloudfrontRequest())).resolves.toEqual( + { + status: 302, + headers: { + location: [{ + key: 'Location', + value: 'https://my-cognito-domain.auth.us-east-1.amazoncognito.com/authorize?redirect_uri=https://d111111abcdef8.cloudfront.net&response_type=code&client_id=123456789qwertyuiop987abcd&state=/lol', + }], + }, + }, + ) + .then(() => { + expect(authenticator._getVerifiedToken).toHaveBeenCalled(); + }); + }); +}); + +/* eslint-disable quotes, comma-dangle */ + +const jwksData = { + "keys": [ + { "kid": "1234example=", "alg": "RS256", "kty": "RSA", "e": "AQAB", "n": "1234567890", "use": "sig" }, + { "kid": "5678example=", "alg": "RS256", "kty": "RSA", "e": "AQAB", "n": "987654321", "use": "sig" }, + ] +}; + +const tokenData = { + "access_token":"eyJz9sdfsdfsdfsd", + "refresh_token":"dn43ud8uj32nk2je", + "id_token":"dmcxd329ujdmkemkd349r", + "token_type":"Bearer", + 'expires_in':3600, +}; + +const getCloudfrontRequest = () => ({ + "Records": [ + { + "cf": { + "config": { + "distributionDomainName": "d123.cloudfront.net", + "distributionId": "EDFDVBD6EXAMPLE", + "eventType": "viewer-request", + "requestId": "MRVMF7KydIvxMWfJIglgwHQwZsbG2IhRJ07sn9AkKUFSHS9EXAMPLE==" + }, + "request": { + "body": { + "action": "read-only", + "data": "eyJ1c2VybmFtZSI6IkxhbWJkYUBFZGdlIiwiY29tbWVudCI6IlRoaXMgaXMgcmVxdWVzdCBib2R5In0=", + "encoding": "base64", + "inputTruncated": false + }, + "clientIp": "2001:0db8:85a3:0:0:8a2e:0370:7334", + "querystring": "", + "uri": "/lol", + "method": "GET", + "headers": { + "host": [ + { + "key": "Host", + "value": "d111111abcdef8.cloudfront.net" + } + ], + "user-agent": [ + { + "key": "User-Agent", + "value": "curl/7.51.0" + }, + ], + "cookie": [ + { + key: 'cookie', + value: `CognitoIdentityServiceProvider.123456789qwertyuiop987abcd.toto.idToken=${tokenData.access_token};` + } + ] + }, + "origin": { + "custom": { + "customHeaders": { + "my-origin-custom-header": [ + { + "key": "My-Origin-Custom-Header", + "value": "Test" + } + ] + }, + "domainName": "example.com", + "keepaliveTimeout": 5, + "path": "/custom_path", + "port": 443, + "protocol": "https", + "readTimeout": 5, + "sslProtocols": [ + "TLSv1", + "TLSv1.1" + ] + }, + "s3": { + "authMethod": "origin-access-identity", + "customHeaders": { + "my-origin-custom-header": [ + { + "key": "My-Origin-Custom-Header", + "value": "Test" + } + ] + }, + "domainName": "my-bucket.s3.amazonaws.com", + "path": "/s3_path", + "region": "us-east-1" + } + } + } + } + } + ] +}); diff --git a/doc/architecture.png b/doc/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..d539346a4facfe431632e3d2a0b02671f4cdfe2a GIT binary patch literal 32245 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 { + if (typeof params[param] !== 'string') { + throw new Error(`Expected params.${param} to be a string`); + } + }); + if (params.cookieExpirationDays && typeof params.cookieExpirationDays !== 'number') { + throw new Error('Expected params.cookieExpirationDays to be a number'); + } + } + + /** + * Download JSON Web Key Set (JWKS) from the UserPool. + * @param {String} issuer URI of the UserPool. + * @return {Promise} Request. + */ + _fetchJWKS() { + this._jwks = {}; + const URL = `${this._issuer}/.well-known/jwks.json`; + this._logger.debug(`Fetching JWKS from ${URL}`); + return axios.get(URL) + .then(resp => { + resp.data.keys.forEach(key => this._jwks[key.kid] = key); + }) + .catch(err => { + this._logger.error(`Unable to fetch JWKS from ${URL}`); + throw err; + }); + } + + /** + * Verify that the current token is valid. Throw an error if not. + * @param {String} token Token to verify. + * @return {Object} Decoded token. + */ + _getVerifiedToken(token) { + this._logger.debug({ msg: 'Verifying token...', token }); + const decoded = jwt.decode(token, {complete: true}); + const kid = decoded.header.kid; + const verified = jwt.verify(token, jwkToPem(this._jwks[kid]), { audience: this._userPoolAppId, issuer: this._issuer }); + assert.strictEqual(verified.token_use, 'id'); + return verified; + } + + /** + * Exchange authorization code for tokens. + * @param {String} redirectURI Redirection URI. + * @param {String} code Authorization code. + * @return {Promise} Authenticated user tokens. + */ + _fetchTokensFromCode(redirectURI, code) { + const request = { + url: `https://${this._userPoolDomain}/oauth2/token`, + method: 'post', + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + data: querystring.stringify({ + client_id: this._userPoolAppId, + code: code, + grant_type: 'authorization_code', + redirect_uri: redirectURI, + }), + }; + this._logger.debug({ msg: 'Fetching tokens from grant code...', request, code }); + return axios.request(request) + .then(resp => { + this._logger.debug({ msg: 'Fetched tokens', tokens: resp.data }); + return resp.data; + }) + .catch(err => { + this._logger.error({ msg: 'Unable to fetch tokens from grant code', request, code }); + throw err; + }); + } + + /** + * Create a Lambda@Edge redirection response to set the tokens on the user's browser cookies. + * @param {Object} tokens Cognito User Pool tokens. + * @param {String} domain Website domain. + * @param {String} location Path to redirection. + * @return {Object} Lambda@Edge response. + */ + _getRedirectResponse(tokens, domain, location) { + const decoded = this._getVerifiedToken(tokens.id_token); + const username = decoded['cognito:username']; + const usernameBase = `${this._cookieBase}.${username}`; + const directives = `Domain=${domain}; Expires=${new Date(new Date() * 1 + this._cookieExpirationDays * 864e+5)}; Secure`; + + const response = { + status: '302' , + headers: { + 'location': [{ key: 'Location', 'value': location }], + 'set-cookie': [ + { + key: 'Set-Cookie', + value: `${usernameBase}.accessToken=${tokens.access_token}; ${directives}`, + }, + { + key: 'Set-Cookie', + value: `${usernameBase}.idToken=${tokens.id_token}; ${directives}`, + }, + { + key: 'Set-Cookie', + value: `${usernameBase}.refreshToken=${tokens.refresh_token}; ${directives}`, + }, + { + key: 'Set-Cookie', + value: `${usernameBase}.tokenScopesString=phone email profile openid aws.cognito.signin.user.admin; ${directives}`, + }, + { + key: 'Set-Cookie', + value: `${this._cookieBase}.LastAuthUser=${username}; ${directives}`, + }, + ], + }, + }; + + this._logger.debug({ msg: 'Generated set-cookie response', response }); + + return response; + } + + /** + * Extract value of the authentication token from the request cookies. + * @param {Array} cookies Request cookies. + * @return {String} Extracted access token. Throw if not found. + */ + _getIdTokenFromCookie(cookies) { + this._logger.debug({ msg: 'Extracting authentication token from request cookie', cookies }); + // eslint-disable-next-line no-useless-escape + const regex = new RegExp(`${this._userPoolAppId}\.[A-z0-9_]*\.idToken=(.*?);`); + if (cookies) { + for (let i = 0; i < cookies.length; i++) { + const matches = cookies[i].value.match(regex); + if (matches && matches.length > 1) { + this._logger.debug({ msg: ' Found token in cookie', token: matches[1] }); + return matches[1]; + } + } + } + this._logger.debug(" idToken wasn't present in request cookies"); + throw new Error("Id token isn't present in the request cookies"); + } + + /** + * Handle Lambda@Edge event: + * * if authentication cookie is present and valid: forward the request + * * if ?code= is present: set cookies with new tokens + * * else redirect to the Cognito UserPool to authenticate the user + * @param {Object} event Lambda@Edge event. + * @return {Promise} CloudFront response. + */ + async handle(event) { + this._logger.debug({ msg: 'Handling Lambda@Edge event', event }); + + if (!this._jwks) { + await this._fetchJWKS(); + } + + const { request } = event.Records[0].cf; + const requestParams = querystring.parse(request.querystring); + const cfDomain = request.headers.host[0].value; + const redirectURI = `https://${cfDomain}`; + + try { + const token = this._getIdTokenFromCookie(request.headers.cookie); + const user = this._getVerifiedToken(token); + this._logger.info({ msg: 'Forwading request', path: request.uri, user }); + return request; + } catch (err) { + this._logger.debug("User isn't authenticated"); + if (requestParams.code) { + return this._fetchTokensFromCode(redirectURI, requestParams.code) + .then(tokens => this._getRedirectResponse(tokens, cfDomain, decodeURIComponent(requestParams.state))); + } else { + let redirectPath = request.uri; + if (request.querystring && request.querystring !== '') { + redirectPath += encodeURIComponent('?' + request.querystring); + } + const userPoolUrl = `https://${this._userPoolDomain}/authorize?redirect_uri=${redirectURI}&response_type=code&client_id=${this._userPoolAppId}&state=${redirectPath}`; + this._logger.debug(`Redirecting user to Cognito User Pool URL ${userPoolUrl}`); + return { + status: 302, + headers: { + location: [{ + key: 'Location', + value: userPoolUrl, + }], + }, + }; + } + } + } +} + +module.exports.Authenticator = Authenticator; diff --git a/package.json b/package.json new file mode 100644 index 0000000..ee829be --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "cognito-at-edge", + "version": "1.0.0", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/cognito-at-edge" + }, + "description": "Serverless authentication solution to protect your website or Amplify application.", + "author": "Hugo David-Boyet ", + "license": "Apache-2.0", + "main": "index.js", + "files": [ + "index.js" + ], + "scripts": { + "test": "jest" + }, + "dependencies": { + "axios": "^0.18.0", + "jsonwebtoken": "^8.2.1", + "jwk-to-pem": "^2.0.0", + "pino": "^5.13.2" + }, + "devDependencies": { + "eslint": "^6.2.2", + "jest": "^24.8.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "keywords": [ + "aws", + "cognito", + "userpool", + "cloudfront", + "lambda", + "edge", + "private", + "website" + ] +}