From 949d3fcbad8e247047922580fe237e537bf50926 Mon Sep 17 00:00:00 2001 From: James Gregory Date: Mon, 13 Apr 2020 08:25:01 +1000 Subject: [PATCH] fix(jwt): sign tokens with real rsa key --- package.json | 2 +- src/keys/cognitoLocal.private.json | 17 ++++++ src/keys/cognitoLocal.public.json | 11 ++++ src/targets/initiateAuth.test.ts | 85 ++++++++++++++++++++++++++++-- src/targets/initiateAuth.ts | 30 +++++++---- tsconfig.json | 1 + 6 files changed, 132 insertions(+), 14 deletions(-) create mode 100644 src/keys/cognitoLocal.private.json create mode 100644 src/keys/cognitoLocal.public.json diff --git a/package.json b/package.json index e9de5f03..a310ac18 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "lint-staged": { "*.ts": [ "eslint --fix", - "tsc --esModuleInterop --noEmit", + "tsc --esModuleInterop --resolveJsonModule --noEmit", "prettier --write" ] }, diff --git a/src/keys/cognitoLocal.private.json b/src/keys/cognitoLocal.private.json new file mode 100644 index 00000000..6ec314ae --- /dev/null +++ b/src/keys/cognitoLocal.private.json @@ -0,0 +1,17 @@ +{ + "jwk": { + "p": "-7Wp_s52Z6Bj5Lggy7ps8LNIFvwFeZkFffT13lsShMUApkEufYsFotqVR7NdUh8eZ2ES-dE_qCo3TRLWe93BKAWlqZ3LbPXCAaQ6hYVk7g-g0KogXG73zY0wnyg5hAU-PMPKiKwbqIC63QzAU7Tn-neiL807BrN4MUQbnLmODU8", + "kty": "RSA", + "q": "3p3rC13eOFSaGy306cFzpApgEwT4UXvT6DCUL3EJZVyDWwyPIuQwiuyso8yJCq210TFDEne6ReZhHEM53T56_17AuXDfo4_xGvWfuVZLRHtS0mxjRzqSoepL76s-ux1jFmRuz_b-FlkR6P-QeW_AFcSoWLycjARQgO2jOfHPWWM", + "d": "TowhslUUsx2bl9fqjV-aOxgnktiSEogBLtU4HJ7dKc3hyzv20YTg09qc__kNZXP4twdxfOgpXBoFDSYZLDdZ6aQGEbG-fktQbH2Ys-LfuNgeI7OroHLnNoWx-fTY6A1Q8nYux8zyRCTGas0F4i3WM9bsN5tYw2QY25w2dTh08yjvp59vGTuiJyjQFhURpXwzIOQ_CtV7FayJhwJD_MaoN8qbquBeDjy2p20YsFwfLrlWSr8bRT54WVAqO4Mu8kG0Q-FGdU9bMDBTvPGAaxr8qzgXXWcoXEM-ByOjsXAcW6m7hhey2vzhfJ-2sSAiLHsQfcALwCuueNxZNzrdZgToyQ", + "e": "AQAB", + "use": "sig", + "kid": "CognitoLocal", + "qi": "Lg-HzxIdHjWkjjbaOba-TnYbixi1Sn9jFGQXJaovzl8nQaNF_qiTNK3yIYBz4cVp9gXfn8FA_kUIMZxq1romb6zp3W2sZkcRD2D40z63pRBkdGknwebSmdy9PnwfqONAFKzgJX514vGhM-e7gfoIrUayiVG-fNG2h4nXR1qnUTw", + "dp": "U3hx0DrdTw4EMmPRFF5VJBj_7gdTNXjGNnfWVQ90e6zswzVYWm-QxemgmW9koggJyBSL-2Ylqvmc7yUxFVB7bm84-Z-HRzHUTUEN2xtaVgu-s5PHOX_fEz4gApePQzWN5w6yilIwtddCoG1LFjcmuouTsDBpw5YeZJAGbBmofsc", + "alg": "RS256", + "dq": "ZNIebkJv7xEZzi9tGSTc67ErO9HnaHftS94cbrQB7l8MuoKgnMu91F1F_tUWR7jOfFSULNv-h8PDvVoQ7ctrRxaxsAqXrmr1ZiFR2k1jvzsfEl-2Qr8bQ6tqAryKp5Gym6SWrycMgjCKtPxxgR4EX5d2KuIZACzADPQTFZ4XK0M", + "n": "2uLO7yh1_6Icfd89V3nNTc_qhfpDN7vEmOYlmJQlc9_RmOns26lg88fXXFntZESwHOm7_homO2Ih6NOtu4P5eskGs8d8VQMOQfF4YrP-pawVz-gh1S7eSvzZRDHBT4ItUuoiVP1B9HN_uScKxIqjmitpPqEQB_o2NJv8npCfqUAU-4KmxquGtjdmfctswSZGdz59M3CAYKDfuvLH9_vV6TRGgbUaUAXWC2WJrbbEXzK3XUDBrmF3Xo-yw8f3SgD3JOPl3HaaWMKL1zGVAsge7gQaGiJBzBurg5vwN61uDGGz0QZC1JqcUTl3cZnrx_L8isIR7074SJEuljIZRnCcjQ" + }, + "pem": "-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEA2uLO7yh1/6Icfd89V3nNTc/qhfpDN7vEmOYlmJQlc9/RmOns\n26lg88fXXFntZESwHOm7/homO2Ih6NOtu4P5eskGs8d8VQMOQfF4YrP+pawVz+gh\n1S7eSvzZRDHBT4ItUuoiVP1B9HN/uScKxIqjmitpPqEQB/o2NJv8npCfqUAU+4Km\nxquGtjdmfctswSZGdz59M3CAYKDfuvLH9/vV6TRGgbUaUAXWC2WJrbbEXzK3XUDB\nrmF3Xo+yw8f3SgD3JOPl3HaaWMKL1zGVAsge7gQaGiJBzBurg5vwN61uDGGz0QZC\n1JqcUTl3cZnrx/L8isIR7074SJEuljIZRnCcjQIDAQABAoIBAE6MIbJVFLMdm5fX\n6o1fmjsYJ5LYkhKIAS7VOBye3SnN4cs79tGE4NPanP/5DWVz+LcHcXzoKVwaBQ0m\nGSw3WemkBhGxvn5LUGx9mLPi37jYHiOzq6By5zaFsfn02OgNUPJ2LsfM8kQkxmrN\nBeIt1jPW7DebWMNkGNucNnU4dPMo76efbxk7oico0BYVEaV8MyDkPwrVexWsiYcC\nQ/zGqDfKm6rgXg48tqdtGLBcHy65Vkq/G0U+eFlQKjuDLvJBtEPhRnVPWzAwU7zx\ngGsa/Ks4F11nKFxDPgcjo7FwHFupu4YXstr84XyftrEgIix7EH3AC8ArrnjcWTc6\n3WYE6MkCgYEA+7Wp/s52Z6Bj5Lggy7ps8LNIFvwFeZkFffT13lsShMUApkEufYsF\notqVR7NdUh8eZ2ES+dE/qCo3TRLWe93BKAWlqZ3LbPXCAaQ6hYVk7g+g0KogXG73\nzY0wnyg5hAU+PMPKiKwbqIC63QzAU7Tn+neiL807BrN4MUQbnLmODU8CgYEA3p3r\nC13eOFSaGy306cFzpApgEwT4UXvT6DCUL3EJZVyDWwyPIuQwiuyso8yJCq210TFD\nEne6ReZhHEM53T56/17AuXDfo4/xGvWfuVZLRHtS0mxjRzqSoepL76s+ux1jFmRu\nz/b+FlkR6P+QeW/AFcSoWLycjARQgO2jOfHPWWMCgYBTeHHQOt1PDgQyY9EUXlUk\nGP/uB1M1eMY2d9ZVD3R7rOzDNVhab5DF6aCZb2SiCAnIFIv7ZiWq+ZzvJTEVUHtu\nbzj5n4dHMdRNQQ3bG1pWC76zk8c5f98TPiACl49DNY3nDrKKUjC110KgbUsWNya6\ni5OwMGnDlh5kkAZsGah+xwKBgGTSHm5Cb+8RGc4vbRkk3OuxKzvR52h37UveHG60\nAe5fDLqCoJzLvdRdRf7VFke4znxUlCzb/ofDw71aEO3La0cWsbAKl65q9WYhUdpN\nY787HxJftkK/G0OragK8iqeRspuklq8nDIIwirT8cYEeBF+XdiriGQAswAz0ExWe\nFytDAoGALg+HzxIdHjWkjjbaOba+TnYbixi1Sn9jFGQXJaovzl8nQaNF/qiTNK3y\nIYBz4cVp9gXfn8FA/kUIMZxq1romb6zp3W2sZkcRD2D40z63pRBkdGknwebSmdy9\nPnwfqONAFKzgJX514vGhM+e7gfoIrUayiVG+fNG2h4nXR1qnUTw=\n-----END RSA PRIVATE KEY-----" +} diff --git a/src/keys/cognitoLocal.public.json b/src/keys/cognitoLocal.public.json new file mode 100644 index 00000000..c4b8fceb --- /dev/null +++ b/src/keys/cognitoLocal.public.json @@ -0,0 +1,11 @@ +{ + "jwt": { + "kty": "RSA", + "e": "AQAB", + "use": "sig", + "kid": "CognitoLocal", + "alg": "RS256", + "n": "2uLO7yh1_6Icfd89V3nNTc_qhfpDN7vEmOYlmJQlc9_RmOns26lg88fXXFntZESwHOm7_homO2Ih6NOtu4P5eskGs8d8VQMOQfF4YrP-pawVz-gh1S7eSvzZRDHBT4ItUuoiVP1B9HN_uScKxIqjmitpPqEQB_o2NJv8npCfqUAU-4KmxquGtjdmfctswSZGdz59M3CAYKDfuvLH9_vV6TRGgbUaUAXWC2WJrbbEXzK3XUDBrmF3Xo-yw8f3SgD3JOPl3HaaWMKL1zGVAsge7gQaGiJBzBurg5vwN61uDGGz0QZC1JqcUTl3cZnrx_L8isIR7074SJEuljIZRnCcjQ" + }, + "pem": "-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEA2uLO7yh1/6Icfd89V3nNTc/qhfpDN7vEmOYlmJQlc9/RmOns26lg\n88fXXFntZESwHOm7/homO2Ih6NOtu4P5eskGs8d8VQMOQfF4YrP+pawVz+gh1S7e\nSvzZRDHBT4ItUuoiVP1B9HN/uScKxIqjmitpPqEQB/o2NJv8npCfqUAU+4KmxquG\ntjdmfctswSZGdz59M3CAYKDfuvLH9/vV6TRGgbUaUAXWC2WJrbbEXzK3XUDBrmF3\nXo+yw8f3SgD3JOPl3HaaWMKL1zGVAsge7gQaGiJBzBurg5vwN61uDGGz0QZC1Jqc\nUTl3cZnrx/L8isIR7074SJEuljIZRnCcjQIDAQAB\n-----END RSA PUBLIC KEY-----" +} diff --git a/src/targets/initiateAuth.test.ts b/src/targets/initiateAuth.test.ts index 5591d32a..3fa01ce8 100644 --- a/src/targets/initiateAuth.test.ts +++ b/src/targets/initiateAuth.test.ts @@ -1,3 +1,5 @@ +import { advanceTo } from "jest-date-mock"; +import * as uuid from "uuid"; import { InvalidPasswordError, NotAuthorizedError, @@ -8,14 +10,21 @@ import { UserPool } from "../services"; import { Triggers } from "../services/triggers"; import { InitiateAuth, InitiateAuthTarget } from "./initiateAuth"; import jwt from "jsonwebtoken"; +import PublicKey from "../keys/cognitoLocal.public.json"; + +const UUID = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i; describe("InitiateAuth target", () => { let initiateAuth: InitiateAuthTarget; let mockDataStore: jest.Mocked; let mockCodeDelivery: jest.Mock; let mockTriggers: jest.Mocked; + let now: Date; beforeEach(() => { + now = new Date(2020, 1, 2, 3, 4, 5); + advanceTo(now); + mockDataStore = { getUserByUsername: jest.fn(), getUserPoolIdForClientId: jest.fn(), @@ -183,14 +192,18 @@ describe("InitiateAuth target", () => { expect(decodedAccessToken).toMatchObject({ client_id: "clientId", - iss: "http://localhost:9229/user-pool-id", + iss: "http://localhost:9229/userPoolId", sub: "0000-0000", token_use: "access", username: "0000-0000", + event_id: expect.stringMatching(UUID), + scope: "aws.cognito.signin.user.admin", // TODO: scopes + auth_time: now.getTime(), + jti: expect.stringMatching(UUID), }); }); - it("generates an id token", async () => { + it("generates an access token that's verifiable with our public key", async () => { mockDataStore.getUserPoolIdForClientId.mockResolvedValue("userPoolId"); mockDataStore.getUserByUsername.mockResolvedValue({ Attributes: [], @@ -211,6 +224,37 @@ describe("InitiateAuth target", () => { }, }); + expect(output).toBeDefined(); + expect(output.AuthenticationResult.AccessToken).toBeDefined(); + + expect( + jwt.verify(output.AuthenticationResult.AccessToken!, PublicKey.pem, { + algorithms: ["RS256"], + }) + ).toBeTruthy(); + }); + + it("generates an id token", async () => { + mockDataStore.getUserPoolIdForClientId.mockResolvedValue("userPoolId"); + mockDataStore.getUserByUsername.mockResolvedValue({ + Attributes: [{ Name: "email", Value: "example@example.com" }], + UserStatus: "CONFIRMED", + Password: "hunter2", + Username: "0000-0000", + Enabled: true, + UserCreateDate: new Date().getTime(), + UserLastModifiedDate: new Date().getTime(), + }); + + const output = await initiateAuth({ + ClientId: "clientId", + AuthFlow: "USER_PASSWORD_AUTH", + AuthParameters: { + USERNAME: "0000-0000", + PASSWORD: "hunter2", + }, + }); + expect(output).toBeDefined(); expect(output.AuthenticationResult.IdToken).toBeDefined(); @@ -218,11 +262,46 @@ describe("InitiateAuth target", () => { expect(decodedIdToken).toMatchObject({ aud: "clientId", - iss: "http://localhost:9229/user-pool-id", + iss: "http://localhost:9229/userPoolId", sub: "0000-0000", token_use: "id", "cognito:username": "0000-0000", + email_verified: true, + event_id: expect.stringMatching(UUID), + auth_time: now.getTime(), + email: "example@example.com", + }); + }); + + it("generates an id token verifiable with our public key", async () => { + mockDataStore.getUserPoolIdForClientId.mockResolvedValue("userPoolId"); + mockDataStore.getUserByUsername.mockResolvedValue({ + Attributes: [{ Name: "email", Value: "example@example.com" }], + UserStatus: "CONFIRMED", + Password: "hunter2", + Username: "0000-0000", + Enabled: true, + UserCreateDate: new Date().getTime(), + UserLastModifiedDate: new Date().getTime(), }); + + const output = await initiateAuth({ + ClientId: "clientId", + AuthFlow: "USER_PASSWORD_AUTH", + AuthParameters: { + USERNAME: "0000-0000", + PASSWORD: "hunter2", + }, + }); + + expect(output).toBeDefined(); + expect(output.AuthenticationResult.IdToken).toBeDefined(); + + expect( + jwt.verify(output.AuthenticationResult.IdToken!, PublicKey.pem, { + algorithms: ["RS256"], + }) + ).toBeTruthy(); }); it.todo("generates a refresh token"); diff --git a/src/targets/initiateAuth.ts b/src/targets/initiateAuth.ts index a872f7cd..b6a9c2bf 100644 --- a/src/targets/initiateAuth.ts +++ b/src/targets/initiateAuth.ts @@ -7,6 +7,8 @@ import { UnsupportedError, } from "../errors"; import { Services } from "../services"; +import PrivateKey from "../keys/cognitoLocal.private.json"; +import * as uuid from "uuid"; interface Input { AuthFlow: "USER_PASSWORD_AUTH" | "CUSTOM_AUTH"; @@ -66,6 +68,9 @@ export const InitiateAuth = ({ throw new InvalidPasswordError(); } + const eventId = uuid.v4(); + const authTime = new Date().getTime(); + return { ChallengeName: "PASSWORD_VERIFIER", ChallengeParameters: {}, @@ -73,37 +78,42 @@ export const InitiateAuth = ({ AccessToken: jwt.sign( { sub: user.Username, - event_id: "439a2a30-ecbc-4788-9ce6-fc6eb9a2d535", + event_id: eventId, token_use: "access", - scope: "aws.cognito.signin.user.admin", - auth_time: 1585450518, - jti: "b398b959-9f2f-40fa-9832-0a237524e460", + scope: "aws.cognito.signin.user.admin", // TODO: scopes + auth_time: authTime, + jti: uuid.v4(), client_id: body.ClientId, username: user.Username, }, - "secret", + PrivateKey.pem, { - issuer: "http://localhost:9229/user-pool-id", + algorithm: "RS256", + issuer: `http://localhost:9229/${userPoolId}`, expiresIn: "24h", + keyid: "CognitoLocal", } ), IdToken: jwt.sign( { sub: user.Username, email_verified: true, - event_id: "439a2a30-ecbc-4788-9ce6-fc6eb9a2d535", + event_id: eventId, token_use: "id", - auth_time: 1585450518, + auth_time: authTime, "cognito:username": user.Username, email: user.Attributes.filter((x) => x.Name === "email").map( (x) => x.Value )[0], }, - "secret", + PrivateKey.pem, { - issuer: "http://localhost:9229/user-pool-id", + algorithm: "RS256", + // TODO: this needs to match the actual host/port we started the server on + issuer: `http://localhost:9229/${userPoolId}`, expiresIn: "24h", audience: body.ClientId, + keyid: "CognitoLocal", } ), RefreshToken: "<< TODO >>", diff --git a/tsconfig.json b/tsconfig.json index 54625240..10108609 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "module": "commonjs", "moduleResolution": "node", "outDir": "./lib", + "resolveJsonModule": true, "strict": true, "target": "ES2019" },