Skip to content

Commit

Permalink
refactor(adm): save user data to Request
Browse files Browse the repository at this point in the history
- also split audit log and auth plugins
  • Loading branch information
MrOrz committed Nov 19, 2024
1 parent 094a95f commit d4407a1
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 30 deletions.
9 changes: 5 additions & 4 deletions src/adm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import path from 'node:path';
import { createRouter, Response } from 'fets';
import { Type } from '@sinclair/typebox';

import { useAuditLog } from './util';
import { useAuditLog, useAuth } from './util';

import pingHandler from './handlers/ping';

Expand All @@ -26,9 +26,10 @@ const router = createRouter({
},
},

// Include audit log plugin and block non-cloudflare requests only in production
//
plugins: shouldAuth ? [useAuditLog()] : [],
plugins: [
...(shouldAuth ? [useAuth()] : []), // block non-cloudflare requests only in production
useAuditLog(),
],
}).route({
method: 'POST',
path: '/ping',
Expand Down
77 changes: 51 additions & 26 deletions src/adm/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,34 @@ const HEADERS_TO_INCLUDE = new Set([
*
* Ref: https://developers.cloudflare.com/cloudflare-one/identity/authorization-cookie/application-token/
* */
const TOKEN_KEYS_TO_INCLUDE = new Set([
const TOKEN_KEYS = [
'aud',
'exp',
'iat',
'country',
'email', // For identity-based auth
'common_name', // For service token auth
]);
] as const;
const TOKEN_KEY_SET = new Set(TOKEN_KEYS as ReadonlyArray<string>);

type CloudflareJWTPayload = JWTPayload & {
country?: string;
email?: string;
common_name?: string;
};

/**
* Attach user & userId to global Reqest
* @ref: https://github.com/ardatan/whatwg-node/blob/master/packages/server-plugin-cookies/src/useCookies.ts
*/
declare global {
interface Request {
/** Currently logged-in admin's token information */
user?: Pick<CloudflareJWTPayload, (typeof TOKEN_KEYS)[number]>;
/** Currently logged-in admin's user ID for use in Cofacts DB */
userId?: string;
}
}

/**
* Verifying Cloudflare Access token.
Expand All @@ -35,26 +55,9 @@ const JWKS = createRemoteJWKSet(new URL(CERTS_URL));

const logger = pino({ name: 'Admin API' });

/**
* Log request, response and user information.
*
* Abort request if it's not coming from Cloudflare Access.
*
* Ref: https://the-guild.dev/openapi/fets/server/plugins
*/
export function useAuditLog(): RouterPlugin<any, any> {
export function useAuth(): RouterPlugin<any, any> {

Check warning on line 58 in src/adm/util.ts

View workflow job for this annotation

GitHub Actions / install-and-test

Unexpected any. Specify a different type

Check warning on line 58 in src/adm/util.ts

View workflow job for this annotation

GitHub Actions / install-and-test

Unexpected any. Specify a different type

Check warning on line 58 in src/adm/util.ts

View workflow job for this annotation

GitHub Actions / install-and-test

Unexpected any. Specify a different type

Check warning on line 58 in src/adm/util.ts

View workflow job for this annotation

GitHub Actions / install-and-test

Unexpected any. Specify a different type
return {
async onRequest({ request }) {
const requestInfo = {
id: request.headers.get('cf-ray'),
url: request.url,
headers: Object.fromEntries(
[...request.headers.entries()].filter(([key]) =>
HEADERS_TO_INCLUDE.has(key)
)
),
};

const token = request.headers.get('cf-access-jwt-assertion');
if (!token) {
throw new HTTPError(
Expand All @@ -65,7 +68,7 @@ export function useAuditLog(): RouterPlugin<any, any> {
);
}

let payload: JWTPayload;
let payload: CloudflareJWTPayload;
try {
payload = (
await jwtVerify(token, JWKS, {
Expand All @@ -77,17 +80,39 @@ export function useAuditLog(): RouterPlugin<any, any> {
throw new HTTPError(403, 'Unauthorized', {});
}

request.user = Object.fromEntries(
Object.entries(payload).filter(([key]) => TOKEN_KEY_SET.has(key))
);
request.userId = payload.email ?? payload.common_name;
},
};
}

/**
* Log request, response and user information.
*
* Abort request if it's not coming from Cloudflare Access.
*
* Ref: https://the-guild.dev/openapi/fets/server/plugins
*/
export function useAuditLog(): RouterPlugin<any, any> {

Check warning on line 98 in src/adm/util.ts

View workflow job for this annotation

GitHub Actions / install-and-test

Unexpected any. Specify a different type

Check warning on line 98 in src/adm/util.ts

View workflow job for this annotation

GitHub Actions / install-and-test

Unexpected any. Specify a different type

Check warning on line 98 in src/adm/util.ts

View workflow job for this annotation

GitHub Actions / install-and-test

Unexpected any. Specify a different type

Check warning on line 98 in src/adm/util.ts

View workflow job for this annotation

GitHub Actions / install-and-test

Unexpected any. Specify a different type
return {
async onRequest({ request }) {
const { url, user, userId } = request;
const shouldIncludeBody =
'common_name' in payload /* Called via service tokens */ ||
'common_name' in (request.user ?? {}) /* Called via service tokens */ ||
request.headers.get('content-type') ===
'application/json'; /* Probably from Swagger UI */

logger.info(
{
...requestInfo,
user: Object.fromEntries(
Object.entries(payload).filter(([key]) =>
TOKEN_KEYS_TO_INCLUDE.has(key)
url,
user,
userId,
id: request.headers.get('cf-ray'),
headers: Object.fromEntries(
[...request.headers.entries()].filter(([key]) =>
HEADERS_TO_INCLUDE.has(key)
)
),
req: shouldIncludeBody ? await request.json() : undefined,
Expand Down

0 comments on commit d4407a1

Please sign in to comment.