From df421d7ca83312f6b643d5ee9d3e9aa0bfff63a4 Mon Sep 17 00:00:00 2001 From: James Gregory Date: Sun, 3 May 2020 10:59:31 +1000 Subject: [PATCH] feat: createUserPoolClient support --- README.md | 29 ++++++-- integration-tests/cognitoClient.test.ts | 36 ++++++++++ integration-tests/userPoolClient.test.ts | 41 +++++------ package.json | 3 +- src/server/defaults.ts | 4 +- src/services/appClient.ts | 15 ++++ src/services/cognitoClient.test.ts | 6 +- src/services/cognitoClient.ts | 11 +-- .../triggers/postConfirmation.test.ts | 1 + src/services/triggers/userMigration.test.ts | 1 + src/services/userPoolClient.test.ts | 54 ++++++++++++-- src/services/userPoolClient.ts | 23 +++++- src/targets/confirmForgotPassword.test.ts | 20 +----- src/targets/confirmForgotPassword.ts | 10 +-- src/targets/confirmSignUp.test.ts | 20 +----- src/targets/confirmSignUp.ts | 10 +-- src/targets/createUserPoolClient.test.ts | 70 +++++++++++++++++++ src/targets/createUserPoolClient.ts | 31 ++++++++ src/targets/forgotPassword.test.ts | 1 + src/targets/forgotPassword.ts | 7 +- src/targets/initiateAuth.test.ts | 17 +---- src/targets/initiateAuth.ts | 5 -- src/targets/listUsers.test.ts | 12 +--- src/targets/listUsers.ts | 5 -- src/targets/router.ts | 2 + src/targets/signUp.test.ts | 1 + src/targets/signUp.ts | 6 +- yarn.lock | 13 ++++ 28 files changed, 311 insertions(+), 143 deletions(-) create mode 100644 integration-tests/cognitoClient.test.ts create mode 100644 src/services/appClient.ts create mode 100644 src/targets/createUserPoolClient.test.ts create mode 100644 src/targets/createUserPoolClient.ts diff --git a/README.md b/README.md index bf49a8ae..c4bb848b 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,17 @@ An offline emulator for [Amazon Cognito](https://aws.amazon.com/cognito/). -The goal for this project is to be _Good Enough_ for local development use, and that's it. Don't expect it to be perfect, because it won't be. +The goal for this project is to be _Good Enough_ for local development use, and that's it. Don't expect it to be +perfect, because it won't be. ## Features -> At this point in time, assume any features listed below are _partially implemented_ based on @jagregory's personal use-cases. If they don't work for you, please raise an issue. +> At this point in time, assume any features listed below are _partially implemented_ based on @jagregory's personal +> use-cases. If they don't work for you, please raise an issue. - [ConfirmForgotPassword](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ConfirmForgotPassword.html) - [ConfirmSignUp](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ConfirmSignUp.html) +- [CreateUserPoolClient](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_CreateUserPoolClient.html) - [ForgotPassword](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ForgotPassword.html) - [InitiateAuth](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_InitiateAuth.html) - [ListUsers](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ListUsers.html) @@ -40,7 +43,8 @@ Additional supported features: Cognito Local will now be listening on `http://localhost:9229`. -You can now update your AWS code to use the local address for Cognito's endpoint. For example, if you're using amazon-cognito-identity-js you can update your `CognitoUserPool` usage to override the endpoint: +You can now update your AWS code to use the local address for Cognito's endpoint. For example, if you're using +amazon-cognito-identity-js you can update your `CognitoUserPool` usage to override the endpoint: ```js new CognitoUserPool({ @@ -53,7 +57,8 @@ You likely only want to do this when you're running locally on your development ## Configuration -You do not need to supply a config unless you need to customise the behaviour of Congito Local. If you are using Lambda triggers, you will definitely need to override `LambdaClient.endpoint` at a minimum. +You do not need to supply a config unless you need to customise the behaviour of Congito Local. If you are using Lambda +triggers, you will definitely need to override `LambdaClient.endpoint` at a minimum. Before starting Cognito Local, create a config file: @@ -95,10 +100,21 @@ The default config is: ### HTTPS endpoints with self-signed certificates -If you need your Lambda endpoint to be HTTPS with a self-signed certificate, you will need to disable certificate verification in Node for Cognito Local. The easiest way to do this is to run Cognito Local with the `NODE_TLS_REJECT_UNAUTHORIZED` environment variable. +If you need your Lambda endpoint to be HTTPS with a self-signed certificate, you will need to disable certificate +verification in Node for Cognito Local. The easiest way to do this is to run Cognito Local with the +`NODE_TLS_REJECT_UNAUTHORIZED` environment variable. NODE_TLS_REJECT_UNAUTHORIZED=0 cognito-local +### User Pools and Clients + +User Pools are stored in `.cognito/db/$userPoolId.json`. As not all API features are supported yet, you'll likely find +yourself needing to manually edit this file to update the User Pool config or users. If you do modify this file, you +will need to restart Cognito Local. + +User Pool Clients are stored in `.cognito/db/clients.json`. You can create new User Pool Clients using the +`CreateUserPoolClient` API. + ## Known Limitations Many. Cognito Local only works for my exact use-case. @@ -114,7 +130,8 @@ Issues I know about: ## Confirmation codes -If you register a new user and they need to confirm their account, Cognito Local will write a message to the console with their confirmation code instead of emailing it to the user. +If you register a new user and they need to confirm their account, Cognito Local will write a message to the console +with their confirmation code instead of emailing it to the user. For example: diff --git a/integration-tests/cognitoClient.test.ts b/integration-tests/cognitoClient.test.ts new file mode 100644 index 00000000..4080d4c8 --- /dev/null +++ b/integration-tests/cognitoClient.test.ts @@ -0,0 +1,36 @@ +import { createCognitoClient } from "../src/services/cognitoClient"; +import { CreateDataStore, createDataStore } from "../src/services/dataStore"; +import { createUserPoolClient } from "../src/services/userPoolClient"; +import fs from "fs"; +import { promisify } from "util"; + +const mkdtemp = promisify(fs.mkdtemp); +const rmdir = promisify(fs.rmdir); + +describe("Cognito Client", () => { + let path: string; + let tmpCreateDataStore: CreateDataStore; + beforeEach(async () => { + path = await mkdtemp("/tmp/cognito-local:"); + tmpCreateDataStore = (id, defaults) => createDataStore(id, defaults, path); + }); + + afterEach(() => + rmdir(path, { + recursive: true, + }) + ); + + it("creates a clients database", async () => { + await createCognitoClient( + { + Id: "local", + UsernameAttributes: [], + }, + tmpCreateDataStore, + createUserPoolClient + ); + + expect(fs.existsSync(`${path}/clients.json`)).toBe(true); + }); +}); diff --git a/integration-tests/userPoolClient.test.ts b/integration-tests/userPoolClient.test.ts index 183de108..3299dc01 100644 --- a/integration-tests/userPoolClient.test.ts +++ b/integration-tests/userPoolClient.test.ts @@ -1,3 +1,7 @@ +import { + CognitoClient, + createCognitoClient, +} from "../src/services/cognitoClient"; import { CreateDataStore, createDataStore } from "../src/services/dataStore"; import { createUserPoolClient, @@ -10,13 +14,22 @@ const mkdtemp = promisify(fs.mkdtemp); const readFile = promisify(fs.readFile); const rmdir = promisify(fs.rmdir); -describe("User Pool", () => { +describe("User Pool Client", () => { let path: string; let tmpCreateDataStore: CreateDataStore; + let cognitoClient: CognitoClient; beforeEach(async () => { path = await mkdtemp("/tmp/cognito-local:"); tmpCreateDataStore = (id, defaults) => createDataStore(id, defaults, path); + cognitoClient = await createCognitoClient( + { + Id: "local", + UsernameAttributes: [], + }, + tmpCreateDataStore, + createUserPoolClient + ); }); afterEach(() => @@ -26,10 +39,7 @@ describe("User Pool", () => { ); it("creates a database", async () => { - await createUserPoolClient( - { Id: "local", UsernameAttributes: [] }, - tmpCreateDataStore - ); + await cognitoClient.getUserPool("local"); expect(fs.existsSync(path + "/local.json")).toBe(true); }); @@ -37,10 +47,7 @@ describe("User Pool", () => { describe("saveUser", () => { it("saves a user with their username as an additional attribute", async () => { const now = new Date().getTime(); - const userPool = await createUserPoolClient( - { Id: "local", UsernameAttributes: [] }, - tmpCreateDataStore - ); + const userPool = await cognitoClient.getUserPool("local"); await userPool.saveUser({ Username: "1", @@ -75,10 +82,7 @@ describe("User Pool", () => { it("updates a user", async () => { const now = new Date().getTime(); - const userPool = await createUserPoolClient( - { Id: "local", UsernameAttributes: [] }, - tmpCreateDataStore - ); + const userPool = await cognitoClient.getUserPool("local"); await userPool.saveUser({ Username: "1", @@ -147,10 +151,7 @@ describe("User Pool", () => { describe("getUserByUsername", () => { let userPool: UserPoolClient; beforeAll(async () => { - userPool = await createUserPoolClient( - { Id: "local", UsernameAttributes: [] }, - tmpCreateDataStore - ); + userPool = await cognitoClient.getUserPool("local"); await userPool.saveUser({ Username: "1", @@ -186,11 +187,7 @@ describe("User Pool", () => { beforeAll(async () => { now = new Date(); - - userPool = await createUserPoolClient( - { Id: "local", UsernameAttributes: [] }, - tmpCreateDataStore - ); + userPool = await cognitoClient.getUserPool("local"); await userPool.saveUser({ Username: "1", diff --git a/package.json b/package.json index e8c55800..6d0e815a 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "integration-test": "jest --config integration-tests/jest.config.js", "integration-test:watch": "jest --config integration-tests/jest.config.js --watch", "lint": "eslint src/**/*.ts && tsc --noEmit", - "start": "babel-node --extensions='.ts' src/bin/start.ts", + "start": "COGNITO_LOCAL_DEVMODE=1 babel-node --extensions='.ts' src/bin/start.ts", "start:watch": "nodemon", "test": "jest", "test:watch": "jest --watch", @@ -60,6 +60,7 @@ "deepmerge": "^4.2.2", "express": "^4.17.1", "jsonwebtoken": "^8.5.1", + "short-uuid": "^3.1.1", "stormdb": "^0.3.0", "uuid": "^7.0.3" }, diff --git a/src/server/defaults.ts b/src/server/defaults.ts index 47af9606..c0ee9154 100644 --- a/src/server/defaults.ts +++ b/src/server/defaults.ts @@ -5,6 +5,7 @@ import { createDataStore } from "../services/dataStore"; import { createLambda } from "../services/lambda"; import { createTriggers } from "../services/triggers"; import { createCognitoClient } from "../services/cognitoClient"; +import { createUserPoolClient } from "../services/userPoolClient"; import { Router } from "../targets/router"; import { loadConfig } from "./config"; import { createServer, Server } from "./server"; @@ -17,7 +18,8 @@ export const createDefaultServer = async (): Promise => { const cognitoClient = await createCognitoClient( config.UserPoolDefaults, - createDataStore + createDataStore, + createUserPoolClient ); const lambdaClient = new AWS.Lambda(config.LambdaClient); const lambda = createLambda(config.TriggerFunctions, lambdaClient); diff --git a/src/services/appClient.ts b/src/services/appClient.ts new file mode 100644 index 00000000..9bee882e --- /dev/null +++ b/src/services/appClient.ts @@ -0,0 +1,15 @@ +import shortUUID from "short-uuid"; + +export interface AppClient { + UserPoolId: string; + ClientName: string; + ClientId: string; + LastModifiedDate: number; + CreationDate: number; + RefreshTokenValidity: number; + AllowedOAuthFlowsUserPoolClient: boolean; +} + +const generator = shortUUID("0123456789abcdefghijklmnopqrstuvwxyz"); + +export const newId = generator.new; diff --git a/src/services/cognitoClient.test.ts b/src/services/cognitoClient.test.ts index a07d6c79..4b4daf74 100644 --- a/src/services/cognitoClient.test.ts +++ b/src/services/cognitoClient.test.ts @@ -49,6 +49,7 @@ describe("Cognito Client", () => { expect(createUserPoolClient).toHaveBeenCalledWith( { Id: "testing", UsernameAttributes: [] }, + mockDataStore, createDataStore ); expect(userPool).toEqual(mockUserPool); @@ -72,7 +73,9 @@ describe("Cognito Client", () => { }); it("creates a user pool by the id in the client config", async () => { - mockDataStore.get.mockResolvedValue("userPoolId"); + mockDataStore.get.mockResolvedValue({ + UserPoolId: "userPoolId", + }); const cognitoClient = await createCognitoClient( { Id: "local", UsernameAttributes: [] }, createDataStore, @@ -84,6 +87,7 @@ describe("Cognito Client", () => { expect(mockDataStore.get).toHaveBeenCalledWith("Clients.testing"); expect(createUserPoolClient).toHaveBeenCalledWith( { Id: "userPoolId", UsernameAttributes: [] }, + mockDataStore, createDataStore ); expect(userPool).toEqual(mockUserPool); diff --git a/src/services/cognitoClient.ts b/src/services/cognitoClient.ts index f66639bd..90c3116b 100644 --- a/src/services/cognitoClient.ts +++ b/src/services/cognitoClient.ts @@ -1,4 +1,5 @@ import { ResourceNotFoundError } from "../errors"; +import { AppClient } from "./appClient"; import { CreateDataStore } from "./dataStore"; import { CreateUserPoolClient, @@ -7,7 +8,7 @@ import { } from "./userPoolClient"; export interface CognitoClient { - getUserPool(userPoolId: string): Promise; + getUserPool(userPoolId: string): Promise; getUserPoolForClientId(clientId: string): Promise; } @@ -22,18 +23,20 @@ export const createCognitoClient = async ( async getUserPool(userPoolId) { return createUserPoolClient( { ...userPoolDefaultOptions, Id: userPoolId }, + clients, createDataStore ); }, async getUserPoolForClientId(clientId) { - const userPoolId = await clients.get(`Clients.${clientId}`); - if (!userPoolId) { + const appClient = await clients.get(`Clients.${clientId}`); + if (!appClient) { throw new ResourceNotFoundError(); } return createUserPoolClient( - { ...userPoolDefaultOptions, Id: userPoolId }, + { ...userPoolDefaultOptions, Id: appClient.UserPoolId }, + clients, createDataStore ); }, diff --git a/src/services/triggers/postConfirmation.test.ts b/src/services/triggers/postConfirmation.test.ts index 29f5c3ba..7b6f99db 100644 --- a/src/services/triggers/postConfirmation.test.ts +++ b/src/services/triggers/postConfirmation.test.ts @@ -15,6 +15,7 @@ describe("PostConfirmation trigger", () => { invoke: jest.fn(), }; mockUserPoolClient = { + createAppClient: jest.fn(), id: "test", getUserByUsername: jest.fn(), listUsers: jest.fn(), diff --git a/src/services/triggers/userMigration.test.ts b/src/services/triggers/userMigration.test.ts index a42eb595..b5842ce4 100644 --- a/src/services/triggers/userMigration.test.ts +++ b/src/services/triggers/userMigration.test.ts @@ -18,6 +18,7 @@ describe("UserMigration trigger", () => { invoke: jest.fn(), }; mockUserPoolClient = { + createAppClient: jest.fn(), id: "test", getUserByUsername: jest.fn(), listUsers: jest.fn(), diff --git a/src/services/userPoolClient.test.ts b/src/services/userPoolClient.test.ts index c85c5661..b07ed2d5 100644 --- a/src/services/userPoolClient.test.ts +++ b/src/services/userPoolClient.test.ts @@ -1,3 +1,4 @@ +import { advanceTo } from "jest-date-mock"; import { CreateDataStore, DataStore } from "./dataStore"; import { attributesFromRecord, @@ -10,10 +11,20 @@ import { } from "./userPoolClient"; describe("User Pool Client", () => { + let mockClientsDataStore: jest.Mocked; let mockDataStore: jest.Mocked; let createDataStore: jest.MockedFunction; + let now: Date; beforeEach(() => { + now = new Date(2020, 1, 2, 3, 4, 5); + advanceTo(now); + + mockClientsDataStore = { + set: jest.fn(), + get: jest.fn(), + getRoot: jest.fn(), + }; mockDataStore = { set: jest.fn(), get: jest.fn(), @@ -25,6 +36,7 @@ describe("User Pool Client", () => { it("creates a database", async () => { await createUserPoolClient( { Id: "local", UsernameAttributes: [] }, + mockClientsDataStore, createDataStore ); @@ -34,12 +46,40 @@ describe("User Pool Client", () => { }); }); + describe("createAppClient", () => { + it("saves an app client", async () => { + const userPool = await createUserPoolClient( + { Id: "local", UsernameAttributes: [] }, + mockClientsDataStore, + createDataStore + ); + + const result = await userPool.createAppClient("clientName"); + + expect(result).toEqual({ + AllowedOAuthFlowsUserPoolClient: false, + ClientId: expect.stringMatching(/^[a-z0-9]{25}$/), + ClientName: "clientName", + CreationDate: now.getTime(), + LastModifiedDate: now.getTime(), + RefreshTokenValidity: 30, + UserPoolId: "local", + }); + + expect(mockClientsDataStore.set).toHaveBeenCalledWith( + `Clients.${result.ClientId}`, + result + ); + }); + }); + describe("saveUser", () => { it("saves a user with their username as an additional attribute", async () => { const now = new Date().getTime(); const userPool = await createUserPoolClient( { Id: "local", UsernameAttributes: [] }, - () => Promise.resolve(mockDataStore) + mockClientsDataStore, + createDataStore ); await userPool.saveUser({ @@ -108,8 +148,10 @@ describe("User Pool Client", () => { return Promise.resolve(null); }); - userPool = await createUserPoolClient(options, () => - Promise.resolve(mockDataStore) + userPool = await createUserPoolClient( + options, + mockClientsDataStore, + createDataStore ); }); @@ -194,8 +236,10 @@ describe("User Pool Client", () => { return Promise.resolve(null); }); - userPool = await createUserPoolClient(options, () => - Promise.resolve(mockDataStore) + userPool = await createUserPoolClient( + options, + mockClientsDataStore, + createDataStore ); }); diff --git a/src/services/userPoolClient.ts b/src/services/userPoolClient.ts index c50f7c2a..edcb803c 100644 --- a/src/services/userPoolClient.ts +++ b/src/services/userPoolClient.ts @@ -1,4 +1,5 @@ -import { CreateDataStore } from "./dataStore"; +import { AppClient, newId } from "./appClient"; +import { CreateDataStore, DataStore } from "./dataStore"; export interface UserAttribute { Name: "sub" | "email" | "phone_number" | "preferred_username" | string; @@ -41,6 +42,7 @@ export interface User { export interface UserPoolClient { readonly id: string; + createAppClient(name: string): Promise; getUserByUsername(username: string): Promise; listUsers(): Promise; saveUser(user: User): Promise; @@ -56,11 +58,13 @@ export interface UserPool { export type CreateUserPoolClient = ( defaultOptions: UserPool, + clientsDataStore: DataStore, createDataStore: CreateDataStore ) => Promise; export const createUserPoolClient = async ( defaultOptions: UserPool, + clientsDataStore: DataStore, createDataStore: CreateDataStore ): Promise => { const dataStore = await createDataStore(defaultOptions.Id, { @@ -71,6 +75,23 @@ export const createUserPoolClient = async ( return { id: defaultOptions.Id, + async createAppClient(name) { + const id = newId(); + const appClient: AppClient = { + ClientId: id, + ClientName: name, + UserPoolId: defaultOptions.Id, + CreationDate: new Date().getTime(), + LastModifiedDate: new Date().getTime(), + AllowedOAuthFlowsUserPoolClient: false, + RefreshTokenValidity: 30, + }; + + await clientsDataStore.set(`Clients.${id}`, appClient); + + return appClient; + }, + async getUserByUsername(username) { console.log("getUserByUsername", username); diff --git a/src/targets/confirmForgotPassword.test.ts b/src/targets/confirmForgotPassword.test.ts index 3e7a328c..017263c8 100644 --- a/src/targets/confirmForgotPassword.test.ts +++ b/src/targets/confirmForgotPassword.test.ts @@ -1,9 +1,5 @@ import { advanceBy, advanceTo } from "jest-date-mock"; -import { - CodeMismatchError, - ResourceNotFoundError, - UserNotFoundError, -} from "../errors"; +import { CodeMismatchError, UserNotFoundError } from "../errors"; import { CognitoClient, UserPoolClient } from "../services"; import { Triggers } from "../services/triggers"; import { @@ -24,6 +20,7 @@ describe("ConfirmForgotPassword target", () => { advanceTo(now); mockUserPoolClient = { + createAppClient: jest.fn(), id: "test", getUserByUsername: jest.fn(), listUsers: jest.fn(), @@ -47,19 +44,6 @@ describe("ConfirmForgotPassword target", () => { }); }); - it("throws if can't find user pool by client id", async () => { - mockCognitoClient.getUserPoolForClientId.mockResolvedValue(null); - - await expect( - confirmForgotPassword({ - ClientId: "clientId", - Username: "janice", - ConfirmationCode: "1234", - Password: "newPassword", - }) - ).rejects.toBeInstanceOf(ResourceNotFoundError); - }); - it("throws if user doesn't exist", async () => { mockUserPoolClient.getUserByUsername.mockResolvedValue(null); diff --git a/src/targets/confirmForgotPassword.ts b/src/targets/confirmForgotPassword.ts index 2164a867..f9d0d9ef 100644 --- a/src/targets/confirmForgotPassword.ts +++ b/src/targets/confirmForgotPassword.ts @@ -1,8 +1,4 @@ -import { - CodeMismatchError, - ResourceNotFoundError, - UserNotFoundError, -} from "../errors"; +import { CodeMismatchError, UserNotFoundError } from "../errors"; import { Services } from "../services"; interface Input { @@ -19,10 +15,6 @@ export const ConfirmForgotPassword = ({ triggers, }: Services): ConfirmForgotPasswordTarget => async (body) => { const userPool = await cognitoClient.getUserPoolForClientId(body.ClientId); - if (!userPool) { - throw new ResourceNotFoundError(); - } - const user = await userPool.getUserByUsername(body.Username); if (!user) { diff --git a/src/targets/confirmSignUp.test.ts b/src/targets/confirmSignUp.test.ts index 3e95d914..3264222f 100644 --- a/src/targets/confirmSignUp.test.ts +++ b/src/targets/confirmSignUp.test.ts @@ -1,9 +1,5 @@ import { advanceBy, advanceTo } from "jest-date-mock"; -import { - CodeMismatchError, - NotAuthorizedError, - ResourceNotFoundError, -} from "../errors"; +import { CodeMismatchError, NotAuthorizedError } from "../errors"; import { CognitoClient, UserPoolClient } from "../services"; import { Triggers } from "../services/triggers"; import { ConfirmSignUp, ConfirmSignUpTarget } from "./confirmSignUp"; @@ -21,6 +17,7 @@ describe("ConfirmSignUp target", () => { advanceTo(now); mockUserPoolClient = { + createAppClient: jest.fn(), id: "test", getUserByUsername: jest.fn(), listUsers: jest.fn(), @@ -44,19 +41,6 @@ describe("ConfirmSignUp target", () => { }); }); - it("throws if can't find user pool by client id", async () => { - mockCognitoClient.getUserPoolForClientId.mockResolvedValue(null); - - await expect( - confirmSignUp({ - ClientId: "clientId", - Username: "janice", - ConfirmationCode: "1234", - ForceAliasCreation: false, - }) - ).rejects.toBeInstanceOf(ResourceNotFoundError); - }); - it("throws if user doesn't exist", async () => { mockUserPoolClient.getUserByUsername.mockResolvedValue(null); diff --git a/src/targets/confirmSignUp.ts b/src/targets/confirmSignUp.ts index 828e9632..e1632816 100644 --- a/src/targets/confirmSignUp.ts +++ b/src/targets/confirmSignUp.ts @@ -1,8 +1,4 @@ -import { - CodeMismatchError, - NotAuthorizedError, - ResourceNotFoundError, -} from "../errors"; +import { CodeMismatchError, NotAuthorizedError } from "../errors"; import { Services } from "../services"; interface Input { @@ -19,10 +15,6 @@ export const ConfirmSignUp = ({ triggers, }: Services): ConfirmSignUpTarget => async (body) => { const userPool = await cognitoClient.getUserPoolForClientId(body.ClientId); - if (!userPool) { - throw new ResourceNotFoundError(); - } - const user = await userPool.getUserByUsername(body.Username); if (!user) { diff --git a/src/targets/createUserPoolClient.test.ts b/src/targets/createUserPoolClient.test.ts new file mode 100644 index 00000000..92044f5e --- /dev/null +++ b/src/targets/createUserPoolClient.test.ts @@ -0,0 +1,70 @@ +import { advanceTo } from "jest-date-mock"; +import { CognitoClient, UserPoolClient } from "../services"; +import { AppClient } from "../services/appClient"; +import { Triggers } from "../services/triggers"; +import { + CreateUserPoolClient, + CreateUserPoolClientTarget, +} from "./createUserPoolClient"; + +describe("CreateUserPoolClient target", () => { + let createUserPoolClient: CreateUserPoolClientTarget; + let mockCognitoClient: jest.Mocked; + let mockUserPoolClient: 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); + + mockUserPoolClient = { + createAppClient: jest.fn(), + id: "test", + getUserByUsername: jest.fn(), + listUsers: jest.fn(), + saveUser: jest.fn(), + }; + mockCognitoClient = { + getUserPool: jest.fn().mockResolvedValue(mockUserPoolClient), + getUserPoolForClientId: jest.fn().mockResolvedValue(mockUserPoolClient), + }; + mockCodeDelivery = jest.fn(); + mockTriggers = { + enabled: jest.fn(), + postConfirmation: jest.fn(), + userMigration: jest.fn(), + }; + + createUserPoolClient = CreateUserPoolClient({ + cognitoClient: mockCognitoClient, + codeDelivery: mockCodeDelivery, + triggers: mockTriggers, + }); + }); + + it("creates a new app client", async () => { + const createdAppClient: AppClient = { + RefreshTokenValidity: 30, + AllowedOAuthFlowsUserPoolClient: false, + LastModifiedDate: new Date().getTime(), + CreationDate: new Date().getTime(), + UserPoolId: "userPoolId", + ClientId: "abc", + ClientName: "clientName", + }; + mockUserPoolClient.createAppClient.mockResolvedValue(createdAppClient); + + const result = await createUserPoolClient({ + ClientName: "clientName", + UserPoolId: "userPoolId", + }); + + expect(mockUserPoolClient.createAppClient).toHaveBeenCalledWith( + "clientName" + ); + + expect(result).toEqual({ UserPoolClient: createdAppClient }); + }); +}); diff --git a/src/targets/createUserPoolClient.ts b/src/targets/createUserPoolClient.ts new file mode 100644 index 00000000..4258a180 --- /dev/null +++ b/src/targets/createUserPoolClient.ts @@ -0,0 +1,31 @@ +import { Services } from "../services"; + +interface Input { + ClientName: string; + UserPoolId: string; +} + +interface Output { + UserPoolClient: { + UserPoolId: string; + ClientName: string; + ClientId: string; + LastModifiedDate: number; + CreationDate: number; + RefreshTokenValidity: number; + AllowedOAuthFlowsUserPoolClient: boolean; + }; +} + +export type CreateUserPoolClientTarget = (body: Input) => Promise; + +export const CreateUserPoolClient = ({ + cognitoClient, +}: Services): CreateUserPoolClientTarget => async (body) => { + const userPool = await cognitoClient.getUserPool(body.UserPoolId); + const appClient = await userPool.createAppClient(body.ClientName); + + return { + UserPoolClient: appClient, + }; +}; diff --git a/src/targets/forgotPassword.test.ts b/src/targets/forgotPassword.test.ts index ef73672b..4d7f4e9c 100644 --- a/src/targets/forgotPassword.test.ts +++ b/src/targets/forgotPassword.test.ts @@ -17,6 +17,7 @@ describe("ForgotPassword target", () => { advanceTo(now); mockUserPoolClient = { + createAppClient: jest.fn(), id: "test", getUserByUsername: jest.fn(), listUsers: jest.fn(), diff --git a/src/targets/forgotPassword.ts b/src/targets/forgotPassword.ts index 7d34b396..d6745370 100644 --- a/src/targets/forgotPassword.ts +++ b/src/targets/forgotPassword.ts @@ -1,4 +1,4 @@ -import { ResourceNotFoundError, UserNotFoundError } from "../errors"; +import { UserNotFoundError } from "../errors"; import { Services } from "../services"; import { DeliveryDetails } from "../services/codeDelivery/codeDelivery"; @@ -18,12 +18,7 @@ export const ForgotPassword = ({ codeDelivery, }: Services): ForgotPasswordTarget => async (body) => { const userPool = await cognitoClient.getUserPoolForClientId(body.ClientId); - if (!userPool) { - throw new ResourceNotFoundError(); - } - const user = await userPool.getUserByUsername(body.Username); - if (!user) { throw new UserNotFoundError(); } diff --git a/src/targets/initiateAuth.test.ts b/src/targets/initiateAuth.test.ts index 1c83d6c2..1ca87b17 100644 --- a/src/targets/initiateAuth.test.ts +++ b/src/targets/initiateAuth.test.ts @@ -3,7 +3,6 @@ import { InvalidPasswordError, NotAuthorizedError, PasswordResetRequiredError, - ResourceNotFoundError, } from "../errors"; import { CognitoClient, UserPoolClient } from "../services"; import { Triggers } from "../services/triggers"; @@ -26,6 +25,7 @@ describe("InitiateAuth target", () => { advanceTo(now); mockUserPoolClient = { + createAppClient: jest.fn(), id: "test", getUserByUsername: jest.fn(), listUsers: jest.fn(), @@ -49,21 +49,6 @@ describe("InitiateAuth target", () => { }); }); - it("throws if can't find user pool by client id", async () => { - mockCognitoClient.getUserPoolForClientId.mockResolvedValue(null); - - await expect( - initiateAuth({ - ClientId: "clientId", - AuthFlow: "USER_PASSWORD_AUTH", - AuthParameters: { - USERNAME: "0000-0000", - PASSWORD: "hunter2", - }, - }) - ).rejects.toBeInstanceOf(ResourceNotFoundError); - }); - describe("USER_PASSWORD_AUTH auth flow", () => { it("throws if password is incorrect", async () => { mockUserPoolClient.getUserByUsername.mockResolvedValue({ diff --git a/src/targets/initiateAuth.ts b/src/targets/initiateAuth.ts index c4a11eb1..18bbeae5 100644 --- a/src/targets/initiateAuth.ts +++ b/src/targets/initiateAuth.ts @@ -3,7 +3,6 @@ import { InvalidPasswordError, NotAuthorizedError, PasswordResetRequiredError, - ResourceNotFoundError, UnsupportedError, } from "../errors"; import { Services } from "../services"; @@ -38,10 +37,6 @@ export const InitiateAuth = ({ } const userPool = await cognitoClient.getUserPoolForClientId(body.ClientId); - if (!userPool) { - throw new ResourceNotFoundError(); - } - let user = await userPool.getUserByUsername(body.AuthParameters.USERNAME); if (!user && triggers.enabled("UserMigration")) { diff --git a/src/targets/listUsers.test.ts b/src/targets/listUsers.test.ts index dc69a174..5911fdaf 100644 --- a/src/targets/listUsers.test.ts +++ b/src/targets/listUsers.test.ts @@ -1,5 +1,4 @@ import { advanceTo } from "jest-date-mock"; -import { ResourceNotFoundError } from "../errors"; import { CognitoClient, UserPoolClient } from "../services"; import { Triggers } from "../services/triggers"; import { ListUsers, ListUsersTarget } from "./listUsers"; @@ -17,6 +16,7 @@ describe("ListUsers target", () => { advanceTo(now); mockUserPoolClient = { + createAppClient: jest.fn(), id: "test", getUserByUsername: jest.fn(), listUsers: jest.fn(), @@ -40,16 +40,6 @@ describe("ListUsers target", () => { }); }); - it("throws if can't find user pool", async () => { - mockCognitoClient.getUserPool.mockResolvedValue(null); - - await expect( - listUsers({ - UserPoolId: "userPoolId", - }) - ).rejects.toBeInstanceOf(ResourceNotFoundError); - }); - it("lists users and removes Cognito Local fields", async () => { mockUserPoolClient.listUsers.mockResolvedValue([ { diff --git a/src/targets/listUsers.ts b/src/targets/listUsers.ts index 12feb98a..a718797f 100644 --- a/src/targets/listUsers.ts +++ b/src/targets/listUsers.ts @@ -1,4 +1,3 @@ -import { ResourceNotFoundError } from "../errors"; import { Services } from "../services"; import { UserAttribute } from "../services/userPoolClient"; @@ -30,10 +29,6 @@ export const ListUsers = ({ cognitoClient, }: Services): ListUsersTarget => async (body) => { const userPool = await cognitoClient.getUserPool(body.UserPoolId); - if (!userPool) { - throw new ResourceNotFoundError(); - } - const users = await userPool.listUsers(); return { diff --git a/src/targets/router.ts b/src/targets/router.ts index 6052aed1..a44a5699 100644 --- a/src/targets/router.ts +++ b/src/targets/router.ts @@ -2,6 +2,7 @@ import { Services } from "../services"; import { UnsupportedError } from "../errors"; import { ConfirmForgotPassword } from "./confirmForgotPassword"; import { ConfirmSignUp } from "./confirmSignUp"; +import { CreateUserPoolClient } from "./createUserPoolClient"; import { ForgotPassword } from "./forgotPassword"; import { InitiateAuth } from "./initiateAuth"; import { ListUsers } from "./listUsers"; @@ -10,6 +11,7 @@ import { SignUp } from "./signUp"; export const Targets = { ConfirmForgotPassword, ConfirmSignUp, + CreateUserPoolClient, ForgotPassword, InitiateAuth, ListUsers, diff --git a/src/targets/signUp.test.ts b/src/targets/signUp.test.ts index 21fb9e32..02dc32ba 100644 --- a/src/targets/signUp.test.ts +++ b/src/targets/signUp.test.ts @@ -19,6 +19,7 @@ describe("SignUp target", () => { advanceTo(now); mockUserPoolClient = { + createAppClient: jest.fn(), id: "test", getUserByUsername: jest.fn(), listUsers: jest.fn(), diff --git a/src/targets/signUp.ts b/src/targets/signUp.ts index cef386eb..7ae3c721 100644 --- a/src/targets/signUp.ts +++ b/src/targets/signUp.ts @@ -1,5 +1,5 @@ import * as uuid from "uuid"; -import { ResourceNotFoundError, UsernameExistsError } from "../errors"; +import { UsernameExistsError } from "../errors"; import { Services } from "../services"; import { DeliveryDetails } from "../services/codeDelivery/codeDelivery"; import { User } from "../services/userPoolClient"; @@ -31,10 +31,6 @@ export const SignUp = ({ // is enabled on the user pool. This will be the default after Feb 2020. // See: https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-managing-errors.html const userPool = await cognitoClient.getUserPoolForClientId(body.ClientId); - if (!userPool) { - throw new ResourceNotFoundError(); - } - const existingUser = await userPool.getUserByUsername(body.Username); if (existingUser) { throw new UsernameExistsError(); diff --git a/yarn.lock b/yarn.lock index 5fc90fd2..b2413691 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1858,6 +1858,11 @@ ansistyles@~0.1.3: resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539" integrity sha1-XeYEFb2gcbs3EnhUyGT0GyMlRTk= +any-base@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/any-base/-/any-base-1.1.0.tgz#ae101a62bc08a597b4c9ab5b7089d456630549fe" + integrity sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg== + any-observable@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" @@ -8523,6 +8528,14 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== +short-uuid@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/short-uuid/-/short-uuid-3.1.1.tgz#3ff427074b5fa7822c3793994d18a7a82e2f73a4" + integrity sha512-7dI69xtJYpTIbg44R6JSgrbDtZFuZ9vAwwmnF/L0PinykbFrhQ7V8omKsQcVw1TP0nYJ7uQp1PN6/aVMkzQFGQ== + dependencies: + any-base "^1.1.0" + uuid "^3.3.2" + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"