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

Add typescript support #26

Merged
merged 2 commits into from
Jan 12, 2022
Merged
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
13 changes: 9 additions & 4 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@
"amd": true,
"jest": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2018
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"rules": {
"indent": ["error", 2],
"linebreak-style": ["error", "unix"],
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ node_modules
package-lock.json
output-template.yml
*.tgz
coverage/
coverage/
dist/
.DS_Store
13 changes: 8 additions & 5 deletions __tests__/index.test.js → __tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
const axios = require('axios');
import axios from 'axios';

jest.mock('axios');

const { Authenticator } = require('../index');
import { Authenticator } from '../src/';

const DATE = new Date('2017');
// @ts-ignore
global.Date = class extends Date {
constructor() {
super();
Expand All @@ -28,7 +29,7 @@ describe('private functions', () => {
});

test('should fetch token', () => {
axios.request.mockResolvedValue({ data: tokenData });
axios.request = jest.fn().mockResolvedValue({ data: tokenData });

return authenticator._fetchTokensFromCode('htt://redirect', 'AUTH_CODE')
.then(res => {
Expand All @@ -37,7 +38,7 @@ describe('private functions', () => {
});

test('should throw if unable to fetch token', () => {
axios.request.mockRejectedValue(new Error('Unexpected error'));
axios.request = jest.fn().mockRejectedValue(new Error('Unexpected error'));
return expect(() => authenticator._fetchTokensFromCode('htt://redirect', 'AUTH_CODE')).rejects.toThrow();
});

Expand Down Expand Up @@ -152,7 +153,9 @@ describe('createAuthenticator', () => {
});

test('should fail when creating authenticator without params', () => {
expect(() => new Authenticator()).toThrow('Expected params');
// @ts-ignore
// ts-ignore is used here to override typescript's type check in the constructor
// this test is still useful when the library is imported to a js file
expect(() => new Authenticator()).toThrow('Expected params');
});

Expand Down
5 changes: 5 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
16 changes: 12 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
"description": "Cognito authentication made easy to protect your website with CloudFront and Lambda@Edge.",
"author": "AWS Builder Labs <[email protected]>",
"license": "Apache-2.0",
"main": "index.js",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"index.js"
"/dist"
],
"scripts": {
"build": "tsc",
"test": "jest --coverage"
},
"dependencies": {
Expand All @@ -21,8 +23,14 @@
"pino": "^6.10.0"
},
"devDependencies": {
"eslint": "^7.17.0",
"jest": "^26.6.3"
"@types/aws-lambda": "^8.10.89",
"@types/jest": "^27.4.0",
"@typescript-eslint/eslint-plugin": "^5.9.1",
"@typescript-eslint/parser": "^5.9.1",
"eslint": "^7.32.0",
"jest": "^27.4.7",
"ts-jest": "^27.1.2",
"typescript": "^4.5.4"
},
"engines": {
"node": ">=10.0.0"
Expand Down
53 changes: 37 additions & 16 deletions index.js → src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
const axios = require('axios');
const querystring = require('querystring');
const pino = require('pino');
const awsJwtVerify = require('aws-jwt-verify');
import axios from 'axios';
import { parse, stringify } from 'querystring';
import pino from 'pino';
import { CognitoJwtVerifier } from 'aws-jwt-verify';
import { CloudFrontRequestEvent } from 'aws-lambda';

class Authenticator {
constructor(params) {
interface AuthenticatorParams {
region: string;
userPoolId: string;
userPoolAppId: string;
userPoolAppSecret?: string;
userPoolDomain: string;
cookieExpirationDays?: number;
disableCookieDomain?: boolean;
logLevel?: 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'silent';
}

export class Authenticator {
_region: string;
_userPoolId: string;
_userPoolAppId: string;
_userPoolAppSecret: string;
_userPoolDomain: string;
_cookieExpirationDays: number;
_disableCookieDomain: boolean;
_cookieBase: string;
_logger;
_jwtVerifier;

constructor(params: AuthenticatorParams) {
this._verifyParams(params);
this._region = params.region;
this._userPoolId = params.userPoolId;
Expand All @@ -18,7 +41,7 @@ class Authenticator {
level: params.logLevel || 'silent', // Default to silent
base: null, //Remove pid, hostname and name logging as not usefull for Lambda
});
this._jwtVerifier = awsJwtVerify.CognitoJwtVerifier.create({
this._jwtVerifier = CognitoJwtVerifier.create({
userPoolId: params.userPoolId,
clientId: params.userPoolAppId,
tokenUse: 'id',
Expand Down Expand Up @@ -57,18 +80,18 @@ class Authenticator {
const authorization = this._userPoolAppSecret && Buffer.from(`${this._userPoolAppId}:${this._userPoolAppSecret}`).toString('base64');
const request = {
url: `https://${this._userPoolDomain}/oauth2/token`,
method: 'post',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
...(authorization && {'Authorization': `Basic ${authorization}`}),
},
data: querystring.stringify({
data: stringify({
client_id: this._userPoolAppId,
code: code,
grant_type: 'authorization_code',
redirect_uri: redirectURI,
}),
};
} as const;
this._logger.debug({ msg: 'Fetching tokens from grant code...', request, code });
return axios.request(request)
.then(resp => {
Expand All @@ -93,8 +116,8 @@ class Authenticator {
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` :
`Expires=${new Date(new Date() * 1 + this._cookieExpirationDays * 864e+5)}; Secure`;
`Domain=${domain}; Expires=${new Date(Date.now() + this._cookieExpirationDays * 864e+5)}; Secure` :
`Expires=${new Date(Date.now() + this._cookieExpirationDays * 864e+5)}; Secure`;
const response = {
status: '302' ,
headers: {
Expand Down Expand Up @@ -170,11 +193,11 @@ class Authenticator {
* @param {Object} event Lambda@Edge event.
* @return {Promise} CloudFront response.
*/
async handle(event) {
async handle(event: CloudFrontRequestEvent) {
this._logger.debug({ msg: 'Handling Lambda@Edge event', event });

const { request } = event.Records[0].cf;
const requestParams = querystring.parse(request.querystring);
const requestParams = parse(request.querystring);
const cfDomain = request.headers.host[0].value;
const redirectURI = `https://${cfDomain}`;

Expand Down Expand Up @@ -217,5 +240,3 @@ class Authenticator {
}
}
}

module.exports.Authenticator = Authenticator;
11 changes: 11 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "esnext",
"declaration": true,
"outDir": "./dist"
},
"include": [
"src/**/*"
]
}