{"version":3,"file":"verify.mjs","names":["jwks: JSONWebKeySet | undefined","jwtHeaders: ProtectedHeaderParameters | undefined","payload: JWTPayload | undefined"],"sources":["../../src/oauth2/verify.ts"],"sourcesContent":["import { betterFetch } from \"@better-fetch/fetch\";\nimport { APIError } from \"better-call\";\nimport type {\n\tJSONWebKeySet,\n\tJWTPayload,\n\tJWTVerifyOptions,\n\tProtectedHeaderParameters,\n} from \"jose\";\nimport {\n\tcreateLocalJWKSet,\n\tdecodeProtectedHeader,\n\tjwtVerify,\n\tUnsecuredJWT,\n} from \"jose\";\nimport { logger } from \"../env\";\n\n/** Last fetched jwks used locally in getJwks @internal */\nlet jwks: JSONWebKeySet | undefined;\n\nexport interface VerifyAccessTokenRemote {\n\t/** Full url of the introspect endpoint. Should end with `/oauth2/introspect` */\n\tintrospectUrl: string;\n\t/** Client Secret */\n\tclientId: string;\n\t/** Client Secret */\n\tclientSecret: string;\n\t/**\n\t * Forces remote verification of a token.\n\t * This ensures attached session (if applicable)\n\t * is also still active.\n\t */\n\tforce?: boolean;\n}\n\n/**\n * Performs local verification of an access token for your APIs.\n *\n * Can also be configured for remote verification.\n */\nexport async function verifyJwsAccessToken(\n\ttoken: string,\n\topts: {\n\t\t/** Jwks url or promise of a Jwks */\n\t\tjwksFetch: string | (() => Promise);\n\t\t/** Verify options */\n\t\tverifyOptions: JWTVerifyOptions &\n\t\t\tRequired>;\n\t},\n) {\n\ttry {\n\t\tconst jwks = await getJwks(token, opts);\n\t\tconst jwt = await jwtVerify(\n\t\t\ttoken,\n\t\t\tcreateLocalJWKSet(jwks),\n\t\t\topts.verifyOptions,\n\t\t);\n\t\t// Return the JWT payload in introspection format\n\t\t// https://datatracker.ietf.org/doc/html/rfc7662#section-2.2\n\t\tif (jwt.payload.azp) {\n\t\t\tjwt.payload.client_id = jwt.payload.azp;\n\t\t}\n\t\treturn jwt.payload;\n\t} catch (error) {\n\t\tif (error instanceof Error) throw error;\n\t\tthrow new Error(error as unknown as string);\n\t}\n}\n\nexport async function getJwks(\n\ttoken: string,\n\topts: {\n\t\t/** Jwks url or promise of a Jwks */\n\t\tjwksFetch: string | (() => Promise);\n\t},\n) {\n\t// Attempt to decode the token and find a matching kid in jwks\n\tlet jwtHeaders: ProtectedHeaderParameters | undefined;\n\ttry {\n\t\tjwtHeaders = decodeProtectedHeader(token);\n\t} catch (error) {\n\t\tif (error instanceof Error) throw error;\n\t\tthrow new Error(error as unknown as string);\n\t}\n\n\tif (!jwtHeaders.kid) throw new Error(\"Missing jwt kid\");\n\n\t// Fetch jwks if not set or has a different kid than the one stored\n\tif (!jwks || !jwks.keys.find((jwk) => jwk.kid === jwtHeaders.kid)) {\n\t\tjwks =\n\t\t\ttypeof opts.jwksFetch === \"string\"\n\t\t\t\t? await betterFetch(opts.jwksFetch, {\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\tAccept: \"application/json\",\n\t\t\t\t\t\t},\n\t\t\t\t\t}).then(async (res) => {\n\t\t\t\t\t\tif (res.error)\n\t\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t\t`Jwks failed: ${res.error.message ?? res.error.statusText}`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\treturn res.data;\n\t\t\t\t\t})\n\t\t\t\t: await opts.jwksFetch();\n\t\tif (!jwks) throw new Error(\"No jwks found\");\n\t}\n\n\treturn jwks;\n}\n\n/**\n * Performs local verification of an access token for your API.\n *\n * Can also be configured for remote verification.\n */\nexport async function verifyAccessToken(\n\ttoken: string,\n\topts: {\n\t\t/** Verify options */\n\t\tverifyOptions: JWTVerifyOptions &\n\t\t\tRequired>;\n\t\t/** Scopes to additionally verify. Token must include all but not exact. */\n\t\tscopes?: string[];\n\t\t/** Required to verify access token locally */\n\t\tjwksUrl?: string;\n\t\t/** If provided, can verify a token remotely */\n\t\tremoteVerify?: VerifyAccessTokenRemote;\n\t},\n) {\n\tlet payload: JWTPayload | undefined;\n\t// Locally verify\n\tif (opts.jwksUrl && !opts?.remoteVerify?.force) {\n\t\ttry {\n\t\t\tpayload = await verifyJwsAccessToken(token, {\n\t\t\t\tjwksFetch: opts.jwksUrl,\n\t\t\t\tverifyOptions: opts.verifyOptions,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tif (error instanceof Error) {\n\t\t\t\tif (error.name === \"TypeError\" || error.name === \"JWSInvalid\") {\n\t\t\t\t\t// likely an opaque token (continue)\n\t\t\t\t} else if (error.name === \"JWTExpired\") {\n\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\tmessage: \"token expired\",\n\t\t\t\t\t});\n\t\t\t\t} else if (error.name === \"JWTInvalid\") {\n\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\tmessage: \"token invalid\",\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tthrow new Error(error as unknown as string);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Remote verify\n\tif (opts?.remoteVerify) {\n\t\tconst { data: introspect, error: introspectError } = await betterFetch<\n\t\t\tJWTPayload & {\n\t\t\t\tactive: boolean;\n\t\t\t}\n\t\t>(opts.remoteVerify.introspectUrl, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\tAccept: \"application/json\",\n\t\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t},\n\t\t\tbody: new URLSearchParams({\n\t\t\t\tclient_id: opts.remoteVerify.clientId,\n\t\t\t\tclient_secret: opts.remoteVerify.clientSecret,\n\t\t\t\ttoken,\n\t\t\t\ttoken_type_hint: \"access_token\",\n\t\t\t}).toString(),\n\t\t});\n\t\tif (introspectError)\n\t\t\tlogger.error(\n\t\t\t\t`Introspection failed: ${introspectError.message ?? introspectError.statusText}`,\n\t\t\t);\n\t\tif (!introspect)\n\t\t\tthrow new APIError(\"INTERNAL_SERVER_ERROR\", {\n\t\t\t\tmessage: \"introspection failed\",\n\t\t\t});\n\t\tif (!introspect.active)\n\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\tmessage: \"token inactive\",\n\t\t\t});\n\t\t// Verifies payload using verify options (token valid through introspect)\n\t\ttry {\n\t\t\tconst unsecuredJwt = new UnsecuredJWT(introspect).encode();\n\t\t\tconst { audience: _audience, ...verifyOptions } = opts.verifyOptions;\n\t\t\tconst verify = introspect.aud\n\t\t\t\t? UnsecuredJWT.decode(unsecuredJwt, opts.verifyOptions)\n\t\t\t\t: UnsecuredJWT.decode(unsecuredJwt, verifyOptions);\n\t\t\tpayload = verify.payload;\n\t\t} catch (error) {\n\t\t\tthrow new Error(error as unknown as string);\n\t\t}\n\t}\n\n\tif (!payload)\n\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\tmessage: `no token payload`,\n\t\t});\n\n\t// Check scopes if provided\n\tif (opts.scopes) {\n\t\tconst validScopes = new Set(\n\t\t\t(payload.scope as string | undefined)?.split(\" \"),\n\t\t);\n\t\tfor (const sc of opts.scopes) {\n\t\t\tif (!validScopes.has(sc)) {\n\t\t\t\tthrow new APIError(\"FORBIDDEN\", {\n\t\t\t\t\tmessage: `invalid scope ${sc}`,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\treturn payload;\n}\n"],"mappings":"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"}