Skip to content

Commit

Permalink
feat: domain with token
Browse files Browse the repository at this point in the history
  • Loading branch information
amakhno committed Apr 1, 2024
1 parent e757ecc commit 508fb05
Show file tree
Hide file tree
Showing 19 changed files with 696 additions and 79 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:14-alpine as builder
FROM node:16-alpine as builder
WORKDIR /app

# dependencies
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@types/body-parser": "^1.19.0",
"@types/cors": "^2.8.6",
"@types/express": "^4.17.13",
"@types/express-handlebars": "6.0.0",
"@types/jest": "^25.2.1",
"@types/jsonwebtoken": "^8.5.6",
"@types/lodash.mergewith": "^4.6.6",
Expand Down Expand Up @@ -67,6 +68,7 @@
"boxen": "^5.1.2",
"cors": "^2.8.5",
"express": "^4.17.1",
"express-handlebars": "^7.1.2",
"jsonwebtoken": "^8.5.1",
"lodash.mergewith": "^4.6.2",
"pino": "^7.11.0",
Expand Down
2 changes: 2 additions & 0 deletions src/__tests__/mockCognitoService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export const newMockCognitoService = (
getUserPoolForClientId: jest.fn().mockResolvedValue(userPoolClient),
listAppClients: jest.fn(),
listUserPools: jest.fn(),
createDomain: jest.fn(),
getDomain: jest.fn(),
});

export const newMockCognitoServiceFactory = (
Expand Down
2 changes: 2 additions & 0 deletions src/__tests__/mockUserPoolService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export const newMockUserPoolService = (
saveUser: jest.fn(),
storeRefreshToken: jest.fn(),
updateOptions: jest.fn(),
savePoolDomain: jest.fn(),
deletePoolDomain: jest.fn(),
});

export const newMockUserPoolServiceFactory = (
Expand Down
6 changes: 6 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ export class UserNotFoundError extends CognitoError {
}
}

export class PoolNotFoundError extends CognitoError {
public constructor(message = "Pool not found.") {
super("PoolNotFoundError", message);
}
}

export class UsernameExistsError extends CognitoError {
public constructor() {
super("UsernameExistsException", "User already exists");
Expand Down
1 change: 1 addition & 0 deletions src/public/bootstrap-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions src/public/sign-in.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
html,
body {
height: 100%;
}

.form-signin {
max-width: 330px;
padding: 1rem;
}

.form-signin .form-floating:focus-within {
z-index: 2;
}

.form-signin input[type="email"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}

.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
33 changes: 17 additions & 16 deletions src/server/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Router } from "./Router";
import { loadConfig } from "./config";
import { createServer, Server } from "./server";
import { CryptoService } from "../services/crypto";
import { TokenRequestHandle } from "../targets/oauth2/tokenTarget";

export const createDefaultServer = async (
logger: pino.Logger
Expand Down Expand Up @@ -63,24 +64,24 @@ export const createDefaultServer = async (
new CryptoService(config.KMSConfig)
);

const messages = new MessagesService(
triggers,
new MessageDeliveryService(new ConsoleMessageSender())
);
const services = {
clock,
cognito: cognitoClient,
config,
messages: messages,
otp,
tokenGenerator: new JwtTokenGenerator(clock, triggers, config.TokenConfig),
triggers,
};
return createServer(
Router({
clock,
cognito: cognitoClient,
config,
messages: new MessagesService(
triggers,
new MessageDeliveryService(new ConsoleMessageSender())
),
otp,
tokenGenerator: new JwtTokenGenerator(
clock,
triggers,
config.TokenConfig
),
triggers,
}),
cognitoClient,
Router(services),
logger,
new TokenRequestHandle(services),
{
development: !!process.env.COGNITO_LOCAL_DEVMODE,
}
Expand Down
143 changes: 136 additions & 7 deletions src/server/server.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,96 @@
import bodyParser from "body-parser";
import cors from "cors";
import express from "express";
import { engine } from "express-handlebars";
import * as http from "http";
import type { Logger } from "pino";
import * as uuid from "uuid";
import { CognitoError, UnsupportedError } from "../errors";
import { CognitoError, PoolNotFoundError, UnsupportedError } from "../errors";
import { Router } from "./Router";
import PublicKey from "../keys/cognitoLocal.public.json";
import Pino from "pino-http";
import path from "path";
import { TokenRequestHandle } from "../targets/oauth2/tokenTarget";
import { CognitoService } from "../services";
import { Context } from "../services/context";

export interface ServerOptions {
port: number;
hostname: string;
development: boolean;
}

interface OpenIdConfiguration {
authorization_endpoint: string;
end_session_endpoint: string;
id_token_signing_alg_values_supported: string[];
issuer: string;
jwks_uri: string;
response_types_supported: string[];
revocation_endpoint: string;
scopes_supported: string[];
subject_types_supported: string[];
token_endpoint: string;
token_endpoint_auth_methods_supported: string[];
userinfo_endpoint: string;
}

class OpenIdConfigurationImpl implements OpenIdConfiguration {
authorization_endpoint: string;
end_session_endpoint: string;
id_token_signing_alg_values_supported: string[];
issuer: string;
jwks_uri: string;
response_types_supported: string[];
revocation_endpoint: string;
scopes_supported: string[];
subject_types_supported: string[];
token_endpoint: string;
token_endpoint_auth_methods_supported: string[];
userinfo_endpoint: string;

constructor(host: string, userPoolId: string, domainName: string) {
host = `http://${host}`;
this.authorization_endpoint = `${host}/${domainName}/oauth2/authorize`;
this.end_session_endpoint = `${host}/${domainName}/logout`;
this.id_token_signing_alg_values_supported = ["RS256"];
this.issuer = `${host}/${userPoolId}`;
this.jwks_uri = `${host}/${userPoolId}/.well-known/jwks.json`;
this.response_types_supported = ["code", "token"];
this.revocation_endpoint = `${host}/${domainName}/oauth2/revoke`;
this.scopes_supported = ["openid", "email", "phone", "profile"];
this.subject_types_supported = ["public"];
this.token_endpoint = `${host}/${domainName}/oauth2/token`;
this.token_endpoint_auth_methods_supported = [
"client_secret_basic",
"client_secret_post",
];
this.userinfo_endpoint = `${host}/${domainName}/oauth2/userinfo`;
}
}

export interface Server {
application: any; // eslint-disable-line
start(options?: Partial<ServerOptions>): Promise<http.Server>;
}

const getDomainByUserPoolId = async (
host: string,
ctx: Context,
cognitoService: CognitoService,
userPoolId: string
): Promise<OpenIdConfiguration> => {
const userPool = await cognitoService.getUserPool(ctx, userPoolId);
if (!userPool) throw new PoolNotFoundError("User Pool not found");
const domain = userPool.options.Domain;
if (!domain) throw new Error("The user pool does not have a domain");
return new OpenIdConfigurationImpl(host, userPoolId, domain);
};

export const createServer = (
cognito: CognitoService,
router: Router,
logger: Logger,
tokenRequestHandle: TokenRequestHandle,
options: Partial<ServerOptions> = {}
): Server => {
const pino = Pino({
Expand All @@ -38,27 +106,88 @@ export const createServer = (

app.use(pino);

// eslint-disable-next-line @typescript-eslint/no-misused-promises
app.engine("handlebars", engine());
app.set("view engine", "handlebars");
const viewsDir = path.join(__dirname, "..", "./views");
app.set("views", viewsDir);

app.get("/", (req, res) => {
res.render("home");
});

app.use(
cors({
origin: "*",
})
);
app.use(
bodyParser.json({
type: "application/x-amz-json-1.1",
})
);

const publicDir = path.join(__dirname, "..", "./public");
app.use(express.static(publicDir));

app.use(express.json({ type: "application/x-amz-json-1.1" }));
app.use(express.urlencoded({ extended: true }));

app.get("/", (req, res) => {
res.render("home");
});
app.post("/login", (req, res) => {
res.status(200).json({ ok: true });
});

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

app.get("/:userPoolId/.well-known/openid-configuration", (req, res) => {
getDomainByUserPoolId(
req.headers.host || "",
{ logger: req.log },
cognito,
req.params.userPoolId
).then(
(val) => {
res.status(200).json(val);
},
(err) => {
res.status(500).json(err);
}
);
});

app.get("/health", (req, res) => {
res.status(200).json({ ok: true });
});

app.post("/:domain/oauth2/token", (req, res) => {
const authHeader = req.headers["authorization"];
if (!authHeader) {
res.status(400).json({ message: "Missing Authorization header" });
return;
}
const authEncoded = authHeader.replace("Basic ", "");
const authDecoded = Buffer.from(authEncoded, "base64").toString();
const indexOfSplitCharacter = authDecoded.indexOf(":");
const username = authDecoded.substring(0, indexOfSplitCharacter);
const password = authDecoded.substring(indexOfSplitCharacter + 1);

tokenRequestHandle
.tokenRequestHandle(
{ logger: req.log },
req.params.domain,
{
username: username,
password: password,
},
req.body
)
.then((token) => {
res.status(200).json(token);
});
});

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

Expand Down
Loading

0 comments on commit 508fb05

Please sign in to comment.