Skip to content

Commit

Permalink
feat(jwt): expose jwk endpoint to support verifying tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
jagregory committed Apr 13, 2020
1 parent 949d3fc commit bc27b86
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 62 deletions.
149 changes: 88 additions & 61 deletions integration-tests/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,79 +10,54 @@ import {
import { createServer } from "../src/server";

describe("HTTP server", () => {
it("errors with missing x-azm-target header", async () => {
const router = jest.fn();
const server = createServer(router);

const response = await supertest(server.application).post("/");

expect(response.status).toEqual(400);
expect(response.body).toEqual({ message: "Missing x-amz-target header" });
});

it("errors with an poorly formatted x-azm-target header", async () => {
const router = jest.fn();
const server = createServer(router);
describe("/", () => {
it("errors with missing x-azm-target header", async () => {
const router = jest.fn();
const server = createServer(router);

const response = await supertest(server.application)
.post("/")
.set("x-amz-target", "bad-format");
const response = await supertest(server.application).post("/");

expect(response.status).toEqual(400);
expect(response.body).toEqual({
message: "Invalid x-amz-target header",
expect(response.status).toEqual(400);
expect(response.body).toEqual({ message: "Missing x-amz-target header" });
});
});

describe("a handled target", () => {
it("returns the output of a matched target", async () => {
const route = jest.fn().mockResolvedValue({
ok: true,
});
const router = (target: string) =>
target === "valid" ? route : () => Promise.reject();
it("errors with an poorly formatted x-azm-target header", async () => {
const router = jest.fn();
const server = createServer(router);

const response = await supertest(server.application)
.post("/")
.set("x-amz-target", "prefix.valid");
.set("x-amz-target", "bad-format");

expect(response.status).toEqual(200);
expect(response.status).toEqual(400);
expect(response.body).toEqual({
ok: true,
message: "Invalid x-amz-target header",
});
});

it("converts UnsupportedErrors from within a target route to a 500 error", async () => {
const route = jest
.fn()
.mockRejectedValue(new UnsupportedError("integration test"));
const router = (target: string) =>
target === "valid" ? route : () => Promise.reject();
const server = createServer(router);
describe("a handled target", () => {
it("returns the output of a matched target", async () => {
const route = jest.fn().mockResolvedValue({
ok: true,
});
const router = (target: string) =>
target === "valid" ? route : () => Promise.reject();
const server = createServer(router);

const response = await supertest(server.application)
.post("/")
.set("x-amz-target", "prefix.valid");
const response = await supertest(server.application)
.post("/")
.set("x-amz-target", "prefix.valid");

expect(response.status).toEqual(500);
expect(response.body).toEqual({
code: "CognitoLocal#Unsupported",
message: "Cognito Local unsupported feature: integration test",
expect(response.status).toEqual(200);
expect(response.body).toEqual({
ok: true,
});
});
});

it.each`
error | code | message
${new CognitoError("CognitoError", "message")} | ${"CognitoError"} | ${"message"}
${new NotAuthorizedError()} | ${"NotAuthorizedException"} | ${"User not authorized"}
${new UsernameExistsError()} | ${"UsernameExistsException"} | ${"User already exists"}
${new CodeMismatchError()} | ${"CodeMismatchException"} | ${"Incorrect confirmation code"}
${new InvalidPasswordError()} | ${"InvalidPasswordException"} | ${"Invalid password"}
`(
"it converts $code to the format Cognito SDK expects",
async ({ error, code, message }) => {
const route = jest.fn().mockRejectedValue(error);
it("converts UnsupportedErrors from within a target route to a 500 error", async () => {
const route = jest
.fn()
.mockRejectedValue(new UnsupportedError("integration test"));
const router = (target: string) =>
target === "valid" ? route : () => Promise.reject();
const server = createServer(router);
Expand All @@ -91,12 +66,64 @@ describe("HTTP server", () => {
.post("/")
.set("x-amz-target", "prefix.valid");

expect(response.status).toEqual(400);
expect(response.status).toEqual(500);
expect(response.body).toEqual({
code: `CognitoLocal#${code}`,
message,
code: "CognitoLocal#Unsupported",
message: "Cognito Local unsupported feature: integration test",
});
}
);
});

it.each`
error | code | message
${new CognitoError("CognitoError", "message")} | ${"CognitoError"} | ${"message"}
${new NotAuthorizedError()} | ${"NotAuthorizedException"} | ${"User not authorized"}
${new UsernameExistsError()} | ${"UsernameExistsException"} | ${"User already exists"}
${new CodeMismatchError()} | ${"CodeMismatchException"} | ${"Incorrect confirmation code"}
${new InvalidPasswordError()} | ${"InvalidPasswordException"} | ${"Invalid password"}
`(
"it converts $code to the format Cognito SDK expects",
async ({ error, code, message }) => {
const route = jest.fn().mockRejectedValue(error);
const router = (target: string) =>
target === "valid" ? route : () => Promise.reject();
const server = createServer(router);

const response = await supertest(server.application)
.post("/")
.set("x-amz-target", "prefix.valid");

expect(response.status).toEqual(400);
expect(response.body).toEqual({
code: `CognitoLocal#${code}`,
message,
});
}
);
});
});

describe("jwks endpoint", () => {
it("responds with our public key", async () => {
const server = createServer(jest.fn());

const response = await supertest(server.application).get(
"/any-user-pool/.well-known/jwks.json"
);

expect(response.status).toEqual(200);
expect(response.body).toEqual({
keys: [
{
alg: "RS256",
e: "AQAB",
kid: "CognitoLocal",
kty: "RSA",
n:
"2uLO7yh1_6Icfd89V3nNTc_qhfpDN7vEmOYlmJQlc9_RmOns26lg88fXXFntZESwHOm7_homO2Ih6NOtu4P5eskGs8d8VQMOQfF4YrP-pawVz-gh1S7eSvzZRDHBT4ItUuoiVP1B9HN_uScKxIqjmitpPqEQB_o2NJv8npCfqUAU-4KmxquGtjdmfctswSZGdz59M3CAYKDfuvLH9_vV6TRGgbUaUAXWC2WJrbbEXzK3XUDBrmF3Xo-yw8f3SgD3JOPl3HaaWMKL1zGVAsge7gQaGiJBzBurg5vwN61uDGGz0QZC1JqcUTl3cZnrx_L8isIR7074SJEuljIZRnCcjQ",
use: "sig",
},
],
});
});
});
});
2 changes: 1 addition & 1 deletion src/keys/cognitoLocal.public.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"jwt": {
"jwk": {
"kty": "RSA",
"e": "AQAB",
"use": "sig",
Expand Down
7 changes: 7 additions & 0 deletions src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import cors from "cors";
import express from "express";
import { CognitoError, unsupported, UnsupportedError } from "../errors";
import { Router } from "../targets/router";
import PublicKey from "../keys/cognitoLocal.public.json";

export interface ServerStartOptions {
port?: number;
Expand All @@ -28,6 +29,12 @@ export const createServer = (router: Router): Server => {
})
);

app.get("/:userPoolId/.well-known/jwks.json", (req, res) => {
res.status(200).json({
keys: [PublicKey.jwk],
});
});

app.post("/", async (req, res) => {
const xAmzTarget = req.headers["x-amz-target"];

Expand Down

0 comments on commit bc27b86

Please sign in to comment.