Skip to content

Commit

Permalink
feat: createUserPoolClient support
Browse files Browse the repository at this point in the history
  • Loading branch information
jagregory committed May 3, 2020
1 parent eaad662 commit df421d7
Show file tree
Hide file tree
Showing 28 changed files with 311 additions and 143 deletions.
29 changes: 23 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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({
Expand All @@ -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:

Expand Down Expand Up @@ -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.
Expand All @@ -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:

Expand Down
36 changes: 36 additions & 0 deletions integration-tests/cognitoClient.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
41 changes: 19 additions & 22 deletions integration-tests/userPoolClient.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import {
CognitoClient,
createCognitoClient,
} from "../src/services/cognitoClient";
import { CreateDataStore, createDataStore } from "../src/services/dataStore";
import {
createUserPoolClient,
Expand All @@ -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(() =>
Expand All @@ -26,21 +39,15 @@ 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);
});

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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
},
Expand Down
4 changes: 3 additions & 1 deletion src/server/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -17,7 +18,8 @@ export const createDefaultServer = async (): Promise<Server> => {

const cognitoClient = await createCognitoClient(
config.UserPoolDefaults,
createDataStore
createDataStore,
createUserPoolClient
);
const lambdaClient = new AWS.Lambda(config.LambdaClient);
const lambda = createLambda(config.TriggerFunctions, lambdaClient);
Expand Down
15 changes: 15 additions & 0 deletions src/services/appClient.ts
Original file line number Diff line number Diff line change
@@ -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;
6 changes: 5 additions & 1 deletion src/services/cognitoClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe("Cognito Client", () => {

expect(createUserPoolClient).toHaveBeenCalledWith(
{ Id: "testing", UsernameAttributes: [] },
mockDataStore,
createDataStore
);
expect(userPool).toEqual(mockUserPool);
Expand All @@ -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,
Expand All @@ -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);
Expand Down
11 changes: 7 additions & 4 deletions src/services/cognitoClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ResourceNotFoundError } from "../errors";
import { AppClient } from "./appClient";
import { CreateDataStore } from "./dataStore";
import {
CreateUserPoolClient,
Expand All @@ -7,7 +8,7 @@ import {
} from "./userPoolClient";

export interface CognitoClient {
getUserPool(userPoolId: string): Promise<UserPoolClient | null>;
getUserPool(userPoolId: string): Promise<UserPoolClient>;
getUserPoolForClientId(clientId: string): Promise<UserPoolClient>;
}

Expand All @@ -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<string>(`Clients.${clientId}`);
if (!userPoolId) {
const appClient = await clients.get<AppClient>(`Clients.${clientId}`);
if (!appClient) {
throw new ResourceNotFoundError();
}

return createUserPoolClient(
{ ...userPoolDefaultOptions, Id: userPoolId },
{ ...userPoolDefaultOptions, Id: appClient.UserPoolId },
clients,
createDataStore
);
},
Expand Down
1 change: 1 addition & 0 deletions src/services/triggers/postConfirmation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe("PostConfirmation trigger", () => {
invoke: jest.fn(),
};
mockUserPoolClient = {
createAppClient: jest.fn(),
id: "test",
getUserByUsername: jest.fn(),
listUsers: jest.fn(),
Expand Down
1 change: 1 addition & 0 deletions src/services/triggers/userMigration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe("UserMigration trigger", () => {
invoke: jest.fn(),
};
mockUserPoolClient = {
createAppClient: jest.fn(),
id: "test",
getUserByUsername: jest.fn(),
listUsers: jest.fn(),
Expand Down
Loading

0 comments on commit df421d7

Please sign in to comment.