Skip to content

Commit b60aafa

Browse files
committed
refactor: replace fast-jwt with jose
1 parent 88bfd30 commit b60aafa

11 files changed

Lines changed: 458 additions & 1261 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/dist/
2+
/.wrangler

server/deployments/cloudflare/wrangler.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ preview_urls = false
99
[observability]
1010
enabled = true
1111

12-
1312
[vars]
1413
GITHUB_APP_ID = "225539"
1514
GITHUB_ACTIONS_TOKEN_ALLOWED_AUDIENCE ="github-actions-access-token.qoomon.workers.dev"

server/package-lock.json

Lines changed: 338 additions & 1035 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,11 @@
1414
"dependencies": {
1515
"@aws-sdk/client-lambda": "^3.971.0",
1616
"@aws-sdk/client-secrets-manager": "^3.972.0",
17-
1817
"@hono/node-server": "^1.19.9",
1918
"@octokit/auth-app": "^8.1.2",
2019
"@octokit/rest": "^22.0.1",
21-
"fast-jwt": "^6.1.0",
22-
"get-jwks": "^11.0.3",
2320
"hono": "^4.11.4",
21+
"jose": "^6.1.3",
2422
"pino": "^10.2.1",
2523
"yaml": "^2.8.2",
2624
"zod": "^4.3.5"

server/src/app.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,14 @@ export function appInit(prepare?: (app: Hono) => void) {
4444

4545
// --- handle access token request -----------------------------------------------------------------------------------
4646
app.post('/access_tokens',
47-
tokenAuthenticator<GitHubActionsJwtPayload>({
48-
allowedIss: 'https://token.actions.githubusercontent.com',
49-
allowedAud: config.githubActionsTokenVerifier.allowedAud,
50-
allowedSub: config.githubActionsTokenVerifier.allowedSub,
51-
}),
47+
tokenAuthenticator<GitHubActionsJwtPayload>(
48+
new URL('https://token.actions.githubusercontent.com/.well-known/jwks'),
49+
{
50+
issuer: 'https://token.actions.githubusercontent.com',
51+
audience: config.githubActionsTokenVerifier.allowedAud,
52+
subjects: config.githubActionsTokenVerifier.allowedSub,
53+
},
54+
),
5255
async (context) => {
5356
const callerIdentity = context.var.token;
5457
logger.info({

server/src/common/github-utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,4 +291,5 @@ export type GitHubActionsJwtPayload = {
291291
run_number: string, // e.g. "107",
292292
run_attempt: string, // e.g. "4",
293293
runner_environment: string, // e.g. "github-hosted",
294-
} & Record<string, string>;
294+
[key: string]: string,
295+
};

server/src/common/hono-utils.ts

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import {HTTPException} from 'hono/http-exception';
44
import type {StatusCode, UnofficialStatusCode} from 'hono/utils/http-status';
55
import {createMiddleware} from 'hono/factory';
66
import {ZodType} from 'zod';
7-
import {createVerifier, KeyFetcher, TokenError, VerifierOptions,} from 'fast-jwt';
87
import {formatZodIssue, JsonTransformer} from './zod-utils.js';
98
import {Status, StatusPhrases} from './http-utils.js';
10-
import {buildJwksKeyFetcher} from './jwt-utils.js';
119
import {indent} from './common-utils.js';
10+
import {createRemoteJWKSet, jwtVerify, JWTVerifyOptions} from 'jose';
11+
import {JOSEError} from 'jose/errors';
1212

1313
/**
1414
* Creates a NotFoundHandler that responses with JSON
@@ -94,14 +94,15 @@ export async function parseJsonBody<T extends ZodType>(req: HonoRequest, schema:
9494

9595
/**
9696
* Creates a middleware that verifies a token and sets the token payload as 'token' context variable
97+
* @param jwksUrl - URL of the JWKS
9798
* @param options - fast-jwt createVerifier options
9899
* @return middleware
99100
*/
100101
export function tokenAuthenticator<T extends object>(
101-
options: Partial<VerifierOptions & { key?: KeyFetcher }>,
102+
jwksUrl: URL,
103+
options: JWTVerifyOptions & { subjects?: RegExp[] },
102104
) {
103-
options.key ??= buildJwksKeyFetcher({providerDiscovery: true});
104-
const verifier = createVerifier(options);
105+
const jwkSet = createRemoteJWKSet(jwksUrl);
105106

106107
return createMiddleware<{ Variables: { token: T } }>(async (context, next) => {
107108
const authorizationHeaderValue = context.req.header().authorization;
@@ -118,23 +119,25 @@ export function tokenAuthenticator<T extends object>(
118119
});
119120
}
120121

121-
const tokenPayload = await verifier(tokenValue)
122-
.catch((error) => {
123-
console.log(`FUCK verifier error`, {
124-
error: error.message,
125-
code: error.code,
126-
originalError: error.originalError?.message,
127-
stack: JSON.stringify(error.originalError?.stack),
128-
}); // TODO remove debug log
129-
if (error instanceof TokenError) {
130-
throw new HTTPException(Status.UNAUTHORIZED, {
131-
message: error.message,
132-
});
133-
}
134-
throw error;
122+
const token = await jwtVerify(tokenValue,
123+
jwkSet, options,
124+
).catch((error) => {
125+
if (error instanceof JOSEError) {
126+
throw new HTTPException(Status.UNAUTHORIZED, {
127+
message: 'Invalid token: ' + error.message,
135128
});
129+
}
130+
throw error;
131+
});
132+
133+
if (options.subjects && !options.subjects
134+
.some((subject) => subject.test(token.payload.sub ?? ''))) {
135+
throw new HTTPException(Status.UNAUTHORIZED, {
136+
message: `Invalid Token: unexpected "sub" claim value`,
137+
});
138+
}
136139

137-
context.set('token', tokenPayload as T);
140+
context.set('token', token.payload as T);
138141

139142
await next();
140143
});

server/src/common/jwt-utils.ts

Lines changed: 0 additions & 24 deletions
This file was deleted.

server/src/logger.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import pino from "pino";
22
import asyncHooks from "node:async_hooks";
33
import process from "process";
4-
import * as crypto from 'node:crypto';
54

65
type Bindings = Record<string, unknown>;
76
const asyncBindings = new asyncHooks.AsyncLocalStorage<{

0 commit comments

Comments
 (0)