1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import * as jose from "https://deno.land/x/jose@v5.2.2/index.ts";
import type { MaybePromise } from "https://esm.town/v/postpostscript/typeUtils";
export type { JWK, JWTPayload } from "https://deno.land/x/jose@v5.2.2/index.ts";
import { getValEndpointFromName, getValNameFromUrl } from "https://esm.town/v/postpostscript/meta";
import { trackUse } from "https://esm.town/v/postpostscript/trackUse";
export type JWTCustomClaims = Record<string, any>;
export type JWTVerifyOptions = jose.JWTVerifyOptions & {
custom?: Record<string, unknown> | ((payload: jose.JWTPayload) => MaybePromise<Record<string, any>>);
customClaimVerifiers?: Record<string, JWTCustomClaimVerifier>;
};
export type JWTCustomClaimVerifier = (payload: jose.JWTPayload, claim: unknown) => MaybePromise<boolean>;
export function createVerifyMethod(keys: () => MaybePromise<jose.JWK[]>) {
return async function verify(
token: string,
options: JWTVerifyOptions,
) {
const publicKey = await jose.importJWK((await keys())[0]);
const { payload } = await jose.jwtVerify(token, publicKey, options);
const customClaims = options.custom && (
options.custom instanceof Function
? await options.custom(payload)
: options.custom
);
await verifyCustomClaims(payload, customClaims || {}, options.customClaimVerifiers ?? defaultCustomClaimVerifiers);
return payload;
};
}
export function createGenerateMethod(keys: () => MaybePromise<jose.JWK[]>) {
return async function generate(payload: any, exp?: string | number | Date) {
const _keys = await keys();
const privateKey = await jose.importJWK(_keys[0]);
let jwt = new jose.SignJWT({
jti: crypto.randomUUID(),
...payload,
})
.setProtectedHeader({ alg: _keys[0].alg })
.setIssuedAt();
if (exp) {
jwt = jwt.setExpirationTime(exp);
}
return jwt.sign(privateKey);
};
}
export async function verifyThirdParty(
token: string,
options: JWTVerifyOptions = {},
) {
const payload = decode(token);
const author = payload.iss?.match(/^@(\w+)\/\w+$/)?.[1];
if (!author) {
throw new Error("issuer does not match the format `@handle/name`");
}
if (payload.exp && payload.exp < (new Date().getTime() / 1000)) {
throw new Error("token has expired");
}
const url = getValEndpointFromName(`@${author}/jwks`);
const method = createVerifyMethod(() => {
return fetch(url)
.then(res => res.json())
.then(({ keys }) => keys);
});
return method(token, options);
}
export function decode(token: string) {
return jose.decodeJwt(token);
}
export const defaultCustomClaimVerifiers: Record<string, JWTCustomClaimVerifier> = {
scope(payload, claimValue) {
const scopes = claimValue instanceof Array
? claimValue
: [claimValue];
const payloadScopes = typeof payload.scope === "string"
? payload.scope.split(" ")
: payload.scope;
if (!(payloadScopes instanceof Array)) {
return false;
}
const payloadScopesSet = new Set(payloadScopes);
return scopes.every(scope => payloadScopesSet.has(scope));
},
async maxUses(payload, claimValue) {
if (typeof claimValue !== "number") {
throw new Error("option custom.maxUses must be a number");
}
if (!payload.jti) {
throw new Error("payload `jti` is not set");
}
await trackUse({
Val Town is a social website to write and deploy JavaScript.
Build APIs and schedule functions from your browser.
Comments
Nobody has commented on this val yet: be the first!
March 7, 2024