Organize directories
This commit is contained in:
parent
8a91c9b89b
commit
4898382f78
433 changed files with 0 additions and 0 deletions
114
apps/metamigo-api/app/plugins/cloudflare-jwt.ts
Normal file
114
apps/metamigo-api/app/plugins/cloudflare-jwt.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import * as Boom from "@hapi/boom";
|
||||
import * as Hoek from "@hapi/hoek";
|
||||
import * as Hapi from "@hapi/hapi";
|
||||
import { promisify } from "util";
|
||||
import jwt from "jsonwebtoken";
|
||||
import jwksClient, { hapiJwt2KeyAsync } from "jwks-rsa";
|
||||
import type { IAppConfig } from "../../config";
|
||||
|
||||
const CF_JWT_HEADER_NAME = "cf-access-jwt-assertion";
|
||||
const CF_JWT_ALGOS = ["RS256"];
|
||||
|
||||
const verifyToken = (settings: any) => {
|
||||
const { audience, issuer } = settings;
|
||||
const client = jwksClient({
|
||||
jwksUri: `${issuer}/cdn-cgi/access/certs`,
|
||||
});
|
||||
|
||||
return async (token: any) => {
|
||||
const getKey = (header: any, callback: any) => {
|
||||
client.getSigningKey(header.kid, (err, key) => {
|
||||
if (err)
|
||||
throw Boom.serverUnavailable(
|
||||
"failed to fetch cloudflare access jwks"
|
||||
);
|
||||
callback(undefined, key?.getPublicKey());
|
||||
});
|
||||
};
|
||||
|
||||
const opts = {
|
||||
algorithms: CF_JWT_ALGOS,
|
||||
audience,
|
||||
issuer,
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (promisify(jwt.verify) as any)(token, getKey, opts);
|
||||
};
|
||||
};
|
||||
|
||||
const handleCfJwt = (verify: any) => async (
|
||||
request: Hapi.Request,
|
||||
h: Hapi.ResponseToolkit
|
||||
) => {
|
||||
const token = request.headers[CF_JWT_HEADER_NAME];
|
||||
if (token) {
|
||||
try {
|
||||
await verify(token);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return Boom.unauthorized("invalid cloudflare access token");
|
||||
}
|
||||
}
|
||||
|
||||
return h.continue;
|
||||
};
|
||||
|
||||
const defaultOpts = {
|
||||
issuer: undefined,
|
||||
audience: undefined,
|
||||
strategyName: "clouflareaccess",
|
||||
validate: undefined,
|
||||
};
|
||||
|
||||
const cfJwtRegister = async (server: Hapi.Server, options: any): Promise<void> => {
|
||||
server.dependency(["hapi-auth-jwt2"]);
|
||||
const settings = Hoek.applyToDefaults(defaultOpts, options);
|
||||
const verify = verifyToken(settings);
|
||||
|
||||
const { validate, strategyName, audience, issuer } = settings;
|
||||
server.ext("onPreAuth", handleCfJwt(verify));
|
||||
|
||||
server.auth.strategy(strategyName!, "jwt", {
|
||||
key: hapiJwt2KeyAsync({
|
||||
jwksUri: `${issuer}/cdn-cgi/access/certs`,
|
||||
}),
|
||||
cookieKey: false,
|
||||
urlKey: false,
|
||||
headerKey: CF_JWT_HEADER_NAME,
|
||||
validate,
|
||||
verifyOptions: {
|
||||
audience,
|
||||
issuer,
|
||||
algorithms: ["RS256"],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const registerCloudflareAccessJwt = async (
|
||||
server: Hapi.Server,
|
||||
config: IAppConfig
|
||||
): Promise<void> => {
|
||||
const { audience, domain } = config.cfaccess;
|
||||
// only enable this plugin if cloudflare access config is configured
|
||||
if (audience && domain) {
|
||||
server.log(["auth"], "cloudflare access authorization enabled");
|
||||
await server.register({
|
||||
plugin: {
|
||||
name: "cloudflare-jwt",
|
||||
version: "0.0.1",
|
||||
register: cfJwtRegister,
|
||||
},
|
||||
options: {
|
||||
issuer: `https://${domain}`,
|
||||
audience,
|
||||
validate: (decoded: any, _request: any) => {
|
||||
const { email, name } = decoded;
|
||||
return {
|
||||
isValid: true,
|
||||
credentials: { user: { email, name } },
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
26
apps/metamigo-api/app/plugins/hapi-nextauth.ts
Normal file
26
apps/metamigo-api/app/plugins/hapi-nextauth.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import type * as Hapi from "@hapi/hapi";
|
||||
import NextAuthPlugin, { AdapterFactory } from "@digiresilience/hapi-nextauth";
|
||||
import { NextAuthAdapter } from "common";
|
||||
import type { SavedUser, UnsavedUser, SavedSession } from "common";
|
||||
import { IAppConfig } from "config";
|
||||
|
||||
export const registerNextAuth = async (
|
||||
server: Hapi.Server,
|
||||
config: IAppConfig
|
||||
): Promise<void> => {
|
||||
// I'm not sure why I need to be so explicit with the generic types here
|
||||
// I thought ts could figure out the generics based on the concrete params, but apparently not
|
||||
const nextAuthAdapterFactory: AdapterFactory<
|
||||
SavedUser,
|
||||
UnsavedUser,
|
||||
SavedSession
|
||||
> = (request: Hapi.Request) => new NextAuthAdapter(request.db());
|
||||
|
||||
await server.register({
|
||||
plugin: NextAuthPlugin,
|
||||
options: {
|
||||
nextAuthAdapterFactory,
|
||||
sharedSecret: config.nextAuth.secret,
|
||||
},
|
||||
});
|
||||
};
|
||||
32
apps/metamigo-api/app/plugins/index.ts
Normal file
32
apps/metamigo-api/app/plugins/index.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import type * as Hapi from "@hapi/hapi";
|
||||
import Schmervice from "@hapipal/schmervice";
|
||||
import PgPromisePlugin from "@digiresilience/hapi-pg-promise";
|
||||
|
||||
import type { IAppConfig } from "../../config";
|
||||
import { dbInitOptions } from "db";
|
||||
import { registerNextAuth } from "./hapi-nextauth";
|
||||
import { registerSwagger } from "./swagger";
|
||||
import { registerNextAuthJwt } from "./nextauth-jwt";
|
||||
import { registerCloudflareAccessJwt } from "./cloudflare-jwt";
|
||||
|
||||
export const register = async (
|
||||
server: Hapi.Server,
|
||||
config: IAppConfig
|
||||
): Promise<void> => {
|
||||
await server.register(Schmervice);
|
||||
|
||||
await server.register({
|
||||
plugin: PgPromisePlugin,
|
||||
options: {
|
||||
// the only required parameter is the connection string
|
||||
connection: config.db.connection,
|
||||
// ... and the pg-promise initialization options
|
||||
pgpInit: dbInitOptions(config),
|
||||
},
|
||||
});
|
||||
|
||||
await registerNextAuth(server, config);
|
||||
await registerSwagger(server);
|
||||
await registerNextAuthJwt(server, config);
|
||||
await registerCloudflareAccessJwt(server, config);
|
||||
};
|
||||
100
apps/metamigo-api/app/plugins/nextauth-jwt.ts
Normal file
100
apps/metamigo-api/app/plugins/nextauth-jwt.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import * as Hoek from "@hapi/hoek";
|
||||
import * as Hapi from "@hapi/hapi";
|
||||
import type { IAppConfig } from "../../config";
|
||||
|
||||
// hapi-auth-jwt2 expects the key to be a raw key
|
||||
const jwkToHapiAuthJwt2 = (jwkString) => {
|
||||
try {
|
||||
const jwk = JSON.parse(jwkString);
|
||||
return Buffer.from(jwk.k, "base64");
|
||||
} catch {
|
||||
throw new Error(
|
||||
"Failed to parse key for JWT verification. This is probably an application configuration error."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const jwtDefaults = {
|
||||
jwkeysB64: undefined,
|
||||
validate: undefined,
|
||||
strategyName: "nextauth-jwt",
|
||||
};
|
||||
|
||||
const jwtRegister = async (server: Hapi.Server, options): Promise<void> => {
|
||||
server.dependency(["hapi-auth-jwt2"]);
|
||||
const settings: any = Hoek.applyToDefaults(jwtDefaults, options);
|
||||
const key = settings.jwkeysB64.map((k) => jwkToHapiAuthJwt2(k));
|
||||
|
||||
server.auth.strategy(settings.strategyName!, "jwt", {
|
||||
key,
|
||||
cookieKey: false,
|
||||
urlKey: false,
|
||||
validate: settings.validate,
|
||||
});
|
||||
};
|
||||
|
||||
export const registerNextAuthJwt = async (
|
||||
server: Hapi.Server,
|
||||
config: IAppConfig
|
||||
): Promise<void> => {
|
||||
if (config.nextAuth.signingKey) {
|
||||
await server.register({
|
||||
plugin: {
|
||||
name: "nextauth-jwt",
|
||||
version: "0.0.2",
|
||||
register: jwtRegister,
|
||||
},
|
||||
options: {
|
||||
jwkeysB64: [config.nextAuth.signingKey],
|
||||
validate: async (decoded, request: Hapi.Request) => {
|
||||
const { email, name, role } = decoded;
|
||||
const user = await request.db().users.findBy({ email });
|
||||
if (!config.isProd) {
|
||||
server.logger.info(
|
||||
{
|
||||
email,
|
||||
name,
|
||||
role,
|
||||
},
|
||||
"nextauth-jwt authorizing request"
|
||||
);
|
||||
// server.logger.info({ user }, "nextauth-jwt user result");
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: Boolean(user && user.isActive),
|
||||
// this credentials object is made available in every request
|
||||
// at `request.auth.credentials`
|
||||
credentials: { email, name, role },
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
} else if (config.isProd) {
|
||||
throw new Error("Missing nextauth.signingKey configuration value.");
|
||||
} else {
|
||||
server.log(
|
||||
["warn"],
|
||||
"Missing nextauth.signingKey configuration value. Authentication of nextauth endpoints disabled!"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// @hapi/jwt expects the key in its own format
|
||||
/* UNUSED
|
||||
const _jwkToHapiJwt = (jwkString) => {
|
||||
try {
|
||||
const jwk = JSON.parse(jwkString);
|
||||
const rawKey = Buffer.from(jwk.k, "base64");
|
||||
return {
|
||||
key: rawKey,
|
||||
algorithms: [jwk.alg],
|
||||
kid: jwk.kid,
|
||||
};
|
||||
} catch {
|
||||
throw new Error(
|
||||
"Failed to parse key for JWT verification. This is probably an application configuration error."
|
||||
);
|
||||
}
|
||||
};
|
||||
*/
|
||||
32
apps/metamigo-api/app/plugins/swagger.ts
Normal file
32
apps/metamigo-api/app/plugins/swagger.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import * as Inert from "@hapi/inert";
|
||||
import * as Vision from "@hapi/vision";
|
||||
import type * as Hapi from "@hapi/hapi";
|
||||
import * as HapiSwagger from "hapi-swagger";
|
||||
|
||||
export const registerSwagger = async (server: Hapi.Server): Promise<void> => {
|
||||
const swaggerOptions: HapiSwagger.RegisterOptions = {
|
||||
info: {
|
||||
title: "Metamigo API Docs",
|
||||
description: "part of CDR Link",
|
||||
version: "0.1",
|
||||
},
|
||||
// group sets of endpoints by tag
|
||||
tags: [
|
||||
{
|
||||
name: "users",
|
||||
description: "API for Users",
|
||||
},
|
||||
],
|
||||
documentationRouteTags: ["swagger"],
|
||||
documentationPath: "/api-docs",
|
||||
};
|
||||
|
||||
await server.register([
|
||||
{ plugin: Inert },
|
||||
{ plugin: Vision },
|
||||
{
|
||||
plugin: HapiSwagger,
|
||||
options: swaggerOptions,
|
||||
},
|
||||
]);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue