diff --git a/.env.example b/.env.example index cd3a88a..31d5587 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,8 @@ # Description: Example of .env file +# Usage: Duplicate this file as .env and change as needed. # MongoDB URI MONGO_URI=mongodb://localhost:27017/example # Auth Plugin -AUTH_DISCOVERY_URL=https://login.microsoftonline.com/c917f3e2-9322-4926-9bb3-daca730413ca/v2.0/.well-known/openid-configuration -AUTH_CLIENT_ID=b4bc4b9a-7162-44c5-bb50-fe935dce1f5a +AUTH_SKIP=false \ No newline at end of file diff --git a/.yarnrc.yml b/.yarnrc.yml index f8c5e49..b2d14bc 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1,11 +1,5 @@ nodeLinker: node-modules -plugins: - # Yarn plugin to enforce engine constraints - - checksum: e5e6e2885ab0e6521b70b0af7c6d8ca2c75dcae2403706fc4600a783b339a6530a476dafb9450c9436ca4050eb6bdee9b62e6e2cebfecf1e81dd709a2480dc07 - path: .yarn/plugins/@yarnpkg/plugin-engines.cjs - spec: "https://raw.githubusercontent.com/devoto13/yarn-plugin-engines/main/bundles/%40yarnpkg/plugin-engines.js" - packageExtensions: # commitlint does not configure its peer dependencies properly... "@commitlint/load@*": @@ -16,3 +10,9 @@ packageExtensions: peerDependencies: "typescript": "*" "@types/node": "*" + +plugins: + # Yarn plugin to enforce engine constraints + - checksum: e5e6e2885ab0e6521b70b0af7c6d8ca2c75dcae2403706fc4600a783b339a6530a476dafb9450c9436ca4050eb6bdee9b62e6e2cebfecf1e81dd709a2480dc07 + path: .yarn/plugins/@yarnpkg/plugin-engines.cjs + spec: "https://raw.githubusercontent.com/devoto13/yarn-plugin-engines/main/bundles/%40yarnpkg/plugin-engines.js" diff --git a/package.json b/package.json index c8ca082..00af7f2 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,10 @@ "test": "tsx --test --experimental-test-coverage test/**/*.test.ts", "compile": "tsgo --noEmit && tsgo -p test/tsconfig.json --noEmit", "lint": "eslint --fix .", + "lint:file": "eslint --fix", "lint:check": "eslint .", "fmt": "prettier --write --list-different .", + "fmt:file": "prettier --write --ignore-unknown", "fmt:check": "prettier --check .", "build": "yarn clean && tsgo", "start": "fastify start --log-level=info --options dist/src/app.js", @@ -19,13 +21,11 @@ }, "lint-staged": { "!(*.{js,ts})": [ - "yarn fmt", - "git add" + "yarn fmt:file" ], "*.{js,ts}": [ - "yarn lint", - "yarn fmt", - "git add" + "yarn lint:file", + "yarn fmt:file" ] }, "engines": { @@ -46,9 +46,7 @@ "fastify-cli": "^7.4.1", "fastify-metrics": "^12.1.0", "fastify-plugin": "^5.1.0", - "jsonwebtoken": "^9.0.3", - "jwks-rsa": "^3.2.2", - "openid-client": "^6.8.2", + "jose": "^6.2.2", "pino-loki": "^3.0.0", "typebox": "~1.1.5" }, @@ -57,7 +55,6 @@ "@commitlint/config-conventional": "^20.4.2", "@eslint/js": "^9.39.2", "@trivago/prettier-plugin-sort-imports": "^6.0.2", - "@types/jsonwebtoken": "^9.0.10", "@types/node": "^25.3.0", "@typescript/native-preview": "^7.0.0-dev.20260219.1", "eslint": "^9.39.2", @@ -72,5 +69,5 @@ "typescript": "^5.9.3", "typescript-eslint": "^8.56.0" }, - "packageManager": "yarn@4.12.0+sha512.f45ab632439a67f8bc759bf32ead036a1f413287b9042726b7cc4818b7b49e14e9423ba49b18f9e06ea4941c1ad062385b1d8760a8d5091a1a31e5f6219afca8" + "packageManager": "yarn@4.13.0+sha512.5c20ba010c99815433e5c8453112165e673f1c7948d8d2b267f4b5e52097538658388ebc9f9580656d9b75c5cc996f990f611f99304a2197d4c56d21eea370e7" } diff --git a/src/app.ts b/src/app.ts index e3eb6fd..ffd017e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -69,8 +69,6 @@ const options: AppOptions = { pluginTimeout: 5 * 60 * 1000, mongoUri: getOption("MONGO_URI")!, - authDiscoveryURL: getOption("AUTH_DISCOVERY_URL")!, - authClientID: getOption("AUTH_CLIENT_ID")!, lokiHost: getOption("LOKI_HOST", false), prometheusKey: getOption("PROMETHEUS_KEY", false), authSkip: getBooleanOption("AUTH_SKIP", false), @@ -210,6 +208,7 @@ const app: FastifyPluginAsync = async ( // through your application void fastify.register(AutoLoad, { dir: path.join(__dirname, "plugins"), + ignorePattern: /(^|[\\/])auth\.ts$/, options: opts, forceESM: true, }); diff --git a/src/plugins/auth.ts b/src/plugins/auth.ts index 4a3f129..67d1a24 100644 --- a/src/plugins/auth.ts +++ b/src/plugins/auth.ts @@ -1,27 +1,61 @@ -import { ResponseSchema } from "../utils/schema.js"; +import { mergeResponse, ResponseSchema } from "../utils/schema.js"; import { UnionOneOf } from "../utils/typebox/union-oneof.js"; -import { FastifyReply, FastifyRequest } from "fastify"; +import { FastifyPluginAsync, FastifyReply, FastifyRequest } from "fastify"; import fp from "fastify-plugin"; -import jwt, { JwtHeader, SigningKeyCallback } from "jsonwebtoken"; -import { JwksClient } from "jwks-rsa"; -import * as client from "openid-client"; -import { skipSubjectCheck, WWWAuthenticateChallengeError } from "openid-client"; +import * as jose from "jose"; import { Type } from "typebox"; +const TenantID = { + "ust.hk": "c917f3e2-9322-4926-9bb3-daca730413ca", + "connect.ust.hk": "6c1d4152-39d0-44ca-88d9-b8d6ddca0708", +} as const; + +type Tenant = keyof typeof TenantID; + +const ClientID = "b4bc4b9a-7162-44c5-bb50-fe935dce1f5a"; + +const jwks = (tenant: Tenant) => + `https://login.microsoftonline.com/${TenantID[tenant]}/discovery/v2.0/keys`; + +const Tenant = (tid: string) => { + for (const tenant in TenantID) { + if (TenantID[tenant as Tenant] === tid) { + return tenant as Tenant; + } + } + throw new Error(`Unknown tenant ID: ${tid}`); +}; + +const parseITSC = (email: string): string => { + const match = email.match(/^([^@]+)@([^@]+)$/); + if (match == null) { + throw new Error(`Invalid email format: ${email}`); + } + const [, local] = match; + return local; +}; + export interface AuthPluginOptions { - /** The discovery URL of the OpenID Connect provider. */ - authDiscoveryURL: string; - /** The client ID of the OpenID Connect client. */ - authClientID: string; /** - * Whether to skip the authentication process. This is useful for testing. - * - * If true, the `authDiscoveryURL` and `authClientID` are not required. Users - * may pass empty strings to pass type checking. + * Whether to skip token verification entirely. This mode is intended for + * local development and tests where an external identity provider may not be + * available. When enabled, the plugin does not require an Authorization + * header and still populates `request.auth` with a fixed identity of + * `user = "usthing"`, `email = "usthing@ust.hk"`, `name = "USThing"`, and + * `tenant = "ust.hk"`. The response also includes `X-Auth-Skip: 1` to make + * bypass mode explicit. */ authSkip?: boolean; } +/** + * Standard authentication error response schema merged into every route by the + * plugin's `onRoute` hook. The plugin appends OpenAPI security requirements + * and these 400/401 response variants to route schemas automatically. The + * 400 responses represent malformed Authorization headers or unsupported + * schemes, while 401 responses represent missing headers and token + * verification or claim-validation failures. + */ export const AuthResponseSchema: ResponseSchema = { 400: UnionOneOf( [ @@ -43,8 +77,8 @@ export const AuthResponseSchema: ResponseSchema = { }), Type.Any({ description: - "The error message from the OpenID Connect provider. " + - "Usually indicates an invalid token. ", + "The error message from token verification or claim validation. " + + "Usually indicates an invalid token.", }), ], { @@ -53,105 +87,65 @@ export const AuthResponseSchema: ResponseSchema = { ), }; -class UnauthorizedError extends Error { - cause: Error; - constructor(cause: Error) { - super(); - this.cause = cause; - this.name = "UnauthorizedError"; - } -} - /** - * The Auth plugin adds authentication ability to the Fastify instance. - * - * For usage, see the /routes/auth-example/index.ts file. - * - * @see authExample + * Auth plugin for Azure AD token verification and request identity decoration. + * The plugin decorates `FastifyRequest` with `request.auth`, injects + * `security: [{ Auth: [] }]` and {@link AuthResponseSchema} into all route + * schemas through `onRoute`, and enforces authentication globally by + * registering a plugin-level `preHandler` hook. Because the plugin is + * encapsulated, it must be applied on the same Fastify scope that defines the + * protected routes, or on an ancestor of that scope. In normal mode, the hook + * expects an `Authorization: Bearer ` header, requires `tid`, + * `unique_name` (email/UPN), and `name` claims, maps `tid` to a known tenant, + * and verifies the token against tenant-specific Microsoft JWKS with the + * configured audience (`ClientID`). Missing headers, malformed header + * formats, invalid schemes, missing required claims, and verification + * failures are returned as 400/401 responses as appropriate. On success, the + * plugin sets `request.auth = { email, user, name, tenant }` (where `email` + * is derived from the `unique_name` claim) and emits `X-Auth-Email`; in skip + * mode it additionally emits `X-Auth-Skip: 1`. */ -export default fp(async (fastify, opts) => { - const skip = opts.authSkip ?? false; +const auth: FastifyPluginAsync = async (fastify, opts) => { + const { authSkip: skip = false } = opts; if (skip) { - fastify.log.warn("Skip Auth: ON"); - } - - const config = await (async () => { - if (skip) { - return null; - } else { - return await client.discovery( - new URL(opts.authDiscoveryURL), - opts.authClientID, - ); - } - })(); - - const key = await (async () => { - if (skip) { - return null; - } else { - fastify.log.info( - { opts, metadata: config!.serverMetadata() }, - "Successfully discovered the OpenID Connect provider.", - ); - - const jwksClient = new JwksClient({ - jwksUri: config!.serverMetadata().jwks_uri ?? "", - }); - return async (header: JwtHeader, callback: SigningKeyCallback) => { - jwksClient.getSigningKey(header.kid, (err, key) => { - const signingKey = key?.getPublicKey(); - callback(err, signingKey); - }); - }; - } - })(); - - async function verify(token: string) { - try { - const info = await new Promise((resolve, reject) => { - jwt.verify(token, key!, (err, info) => { - if (err) { - return reject(err); - } - resolve(info as jwt.JwtPayload); - }); - }); - return info.email && getUsernameFromEmail(info.email); - } catch (e) { - if ( - e instanceof jwt.JsonWebTokenError || - e instanceof jwt.TokenExpiredError || - e instanceof jwt.NotBeforeError - ) { - throw new UnauthorizedError(e); - } - throw e; - } - } - - async function verifyLegacy(token: string) { - try { - const info = await client.fetchUserInfo(config!, token, skipSubjectCheck); - return info.email && getUsernameFromEmail(info.email); - } catch (e) { - if ( - e instanceof WWWAuthenticateChallengeError && - e.response?.status === 401 - ) { - throw new UnauthorizedError(new Error(await e.response.text())); - } - throw e; - } + fastify.log.warn("[AuthPlugin] SKIP_AUTH is on."); } - fastify.decorateRequest("user", undefined); - fastify.decorate( - "authPlugin", + fastify.addHook("onRoute", (routeOptions) => { + routeOptions.schema = routeOptions.schema || {}; + routeOptions.schema.security = routeOptions.schema.security || []; + routeOptions.schema.security = [ + ...routeOptions.schema.security, + { Auth: [] }, + ]; + routeOptions.schema.response = routeOptions.schema.response || {}; + routeOptions.schema.response = mergeResponse([ + routeOptions.schema.response as never, + AuthResponseSchema, + ]); + }); + + const JWKS = { + "ust.hk": jose.createRemoteJWKSet(new URL(jwks("ust.hk"))), + "connect.ust.hk": jose.createRemoteJWKSet(new URL(jwks("connect.ust.hk"))), + }; + + fastify.decorateRequest("auth"); + fastify.addHook( + "preHandler", async function (request: FastifyRequest, reply: FastifyReply) { - if (skip) return; - if (!key) return; + if (skip) { + request.auth = { + email: "usthing@ust.hk", + user: "usthing", + name: "USThing", + tenant: "ust.hk", + }; + reply.header("X-Auth-Skip", "1"); + reply.header("X-Auth-Email", request.auth.email); + return; + } // Extract the authorization header from the request const { authorization } = request.headers; @@ -160,52 +154,93 @@ export default fp(async (fastify, opts) => { } // Extract the scheme and token from the authorization header - const parts = authorization.split(" "); + const parts = authorization.trim().split(/\s+/); if (parts.length !== 2) { return reply.status(400).send("Invalid Authorization Header"); } const [type, token] = parts; - if (type !== "Bearer") { + if (type.toLowerCase() !== "bearer") { return reply.status(400).send("Invalid Authorization Scheme"); } try { - // Verify the token and set the user in the request. - // If the token cannot be verified by the modern method, - // fall back to the legacy method. - request.user = await verify(token).catch(async (e) => { - if (e instanceof UnauthorizedError) { - fastify.log.debug( - "Modern verification failed, falling back to legacy method.", - ); - return await verifyLegacy(token).catch(() => { - throw e; - }); - } - throw e; + const jwt = jose.decodeJwt(token); + if (jwt.tid == undefined || typeof jwt.tid !== "string") { + return reply + .status(401) + .send(`Invalid Token: missing or invalid tid claim ${jwt.tid}`); + } + + const tenant = Tenant(jwt.tid); + + // The issuer is equivalently verified above by looking at the `tid` + // claim and using the corresponding JWKS. + await jose.jwtVerify(token, JWKS[tenant], { + audience: ClientID, }); + + if ( + jwt.unique_name == undefined || + typeof jwt.unique_name !== "string" + ) { + return reply + .status(401) + .send( + `Invalid Token: missing or invalid unique_name claim ${jwt.unique_name}`, + ); + } + + if (jwt.name == undefined || typeof jwt.name !== "string") { + return reply + .status(401) + .send(`Invalid Token: missing or invalid name claim ${jwt.name}`); + } + + const user = parseITSC(jwt.unique_name); + + request.auth = { + email: jwt.unique_name, + user: user, + name: jwt.name, + tenant, + }; + reply.header("X-Auth-Email", request.auth.email); } catch (e) { - if (e instanceof UnauthorizedError) { - const cause = e.cause; - return reply.status(401).send(`${cause.name}: ${cause.message}`); + if (e instanceof Error) { + return reply.status(401).send(`Invalid Token: ${e.message}`); } throw e; } }, ); -}); +}; -function getUsernameFromEmail(email: string): string { - const [username] = email.split("@"); - return username; -} +export default fp(auth, { + name: "auth", + encapsulate: true, +}); declare module "fastify" { - export interface FastifyInstance { - authPlugin(request: FastifyRequest, reply: FastifyReply): Promise; - } - export interface FastifyRequest { - user?: string; + /** + * The `request.auth` object populated by the Auth plugin's `preHandler` + * hook. It contains the authenticated user's email, name, tenant, and a + * derived `user` field which is the ITSC (the local part of the email). The + * presence of this object indicates successful authentication; if + * authentication fails, the request is rejected with a 400/401 response + * before reaching any route handlers. In skip mode, this object is still + * populated with a fixed identity for testing purposes. + * + * For ease of use, the plugin is not marked as optional, which means + * accessing it in routes without the plugin still typechecks. Programmers + * should be aware that to only use `request.auth` in routes that are + * registered after the plugin. + */ + auth: { + email: string; + user: string; + name: string; + tenant: Tenant; + }; } } diff --git a/src/routes/auth-example/index.ts b/src/routes/auth-example/index.ts index 2dc6144..6d9a354 100644 --- a/src/routes/auth-example/index.ts +++ b/src/routes/auth-example/index.ts @@ -1,6 +1,5 @@ import { FastifyTypebox } from "../../app.js"; -import { AuthResponseSchema } from "../../plugins/auth.js"; -import { mergeResponse } from "../../utils/schema.js"; +import auth from "../../plugins/auth.js"; import { FastifyPluginAsync } from "fastify"; import { Type } from "typebox"; @@ -8,77 +7,63 @@ const authExample: FastifyPluginAsync = async ( fastify: FastifyTypebox, opts, ): Promise => { - // Applying authPlugin to this route - fastify.get( - "/", - { - schema: { - summary: "Auth Example", - tags: ["Auth"], - security: [{ Auth: [] }], - response: mergeResponse([ - { + await fastify.register(async function (fastify: FastifyTypebox) { + await auth(fastify, opts); + + fastify.get( + "/", + { + schema: { + summary: "Auth Example", + tags: ["Auth"], + response: { 200: Type.String(), }, - AuthResponseSchema, - ]), + }, + }, + async function (request, reply) { + return `${request.auth.user} is authenticated`; }, - preHandler: fastify.authPlugin, - }, - async function (request, reply) { - return `${request.user} is authenticated`; - }, - ); + ); - // Applying authPlugin to the prefix /sub-auth - fastify.register( - async function (fastify) { - fastify.addHook("preHandler", fastify.authPlugin); - fastify.get( - "/", - { - schema: { - summary: "Sub Auth Example 1", - tags: ["Auth"], - security: [{ Auth: [] }], - response: mergeResponse([ - { - 200: Type.String(), + fastify.register( + async function (fastify: FastifyTypebox) { + fastify.get( + "/", + { + schema: { + summary: "Sub Auth Example 1", + tags: ["Auth"], + response: { 400: Type.String(), }, - AuthResponseSchema, - ]), + }, }, - }, - async function (request, reply) { - return reply.status(400).send("this is a sub auth example 1"); - }, - ); - fastify.get( - "/auth", - { - schema: { - summary: "Sub Auth Example 2", - tags: ["Auth"], - security: [{ Auth: [] }], - response: mergeResponse([ - { - 200: Type.String(), + async function (request, reply) { + return reply.status(400).send("this is a sub auth example 1 (400)"); + }, + ); + fastify.get( + "/auth", + { + schema: { + summary: "Sub Auth Example 2", + tags: ["Auth"], + response: { 401: Type.String(), }, - AuthResponseSchema, - ]), + }, }, - }, - async function (request, reply) { - return reply.status(401).send("this is a sub auth example 2"); - }, - ); - }, - { - prefix: "/sub-auth", - }, - ); + async function (request, reply) { + return reply.status(401).send("this is a sub auth example 2 (401)"); + }, + ); + }, + { + prefix: "/sub-auth", + }, + ); + }); }; export default authExample; diff --git a/test/helper.ts b/test/helper.ts index 9af77a9..f81a6ea 100644 --- a/test/helper.ts +++ b/test/helper.ts @@ -14,8 +14,6 @@ async function config(mongoUri: string): Promise { return { pluginTimeout: options.pluginTimeout, mongoUri, - authDiscoveryURL: "", - authClientID: "", authSkip: true, }; } diff --git a/test/plugins/auth.test.ts b/test/plugins/auth.test.ts index 5b8da43..3ddc5e9 100644 --- a/test/plugins/auth.test.ts +++ b/test/plugins/auth.test.ts @@ -1,7 +1,6 @@ -import Auth from "../../src/plugins/auth.js"; +import auth from "../../src/plugins/auth.js"; import Fastify, { FastifyInstance } from "fastify"; import * as assert from "node:assert"; -import * as process from "node:process"; import { afterEach, beforeEach, suite, test } from "node:test"; await suite("auth plugin", async () => { @@ -9,16 +8,10 @@ await suite("auth plugin", async () => { beforeEach(async () => { fastify = Fastify(); - await fastify.register(Auth, { - authDiscoveryURL: - "https://login.microsoftonline.com/c917f3e2-9322-4926-9bb3-daca730413ca/v2.0/.well-known/openid-configuration", - authClientID: "b4bc4b9a-7162-44c5-bb50-fe935dce1f5a", + await fastify.register(async function (fastify) { + await auth(fastify, {}); + fastify.get("/secret", async (request) => request.auth.user); }); - fastify.get( - "/secret", - { preHandler: fastify.authPlugin }, - async (request) => request.user, - ); await fastify.ready(); }); afterEach(async () => { @@ -60,32 +53,49 @@ await suite("auth plugin", async () => { method: "GET", url: "/secret", headers: { - Authorization: "Bearer INVALID", + Authorization: "Bearer e30.e30.e30", }, }); // Unauthorized assert.equal(response.statusCode, 401); }); - const token = process.env.TEST_AUTH_TOKEN; - const user = process.env.TEST_AUTH_USER; + await test("authorization scheme is case-insensitive", async () => { + const response = await fastify.inject({ + method: "GET", + url: "/secret", + headers: { + Authorization: "bearer e30.e30.e30", + }, + }); + // Unauthorized because the token is invalid, not because the scheme casing is wrong. + assert.equal(response.statusCode, 401); + }); - await test( - "valid token", - { skip: token === undefined || user === undefined }, - async () => { - const response = await fastify.inject({ - method: "GET", - url: "/secret", - headers: { - Authorization: `Bearer ${token}`, - }, - }); - // OK - assert.equal(response.statusCode, 200); - assert.equal(response.body, user); - }, - ); + await test("authorization header tolerates repeated spaces", async () => { + const response = await fastify.inject({ + method: "GET", + url: "/secret", + headers: { + Authorization: " Bearer e30.e30.e30 ", + }, + }); + // Unauthorized because the token is invalid, not because whitespace parsing failed. + assert.equal(response.statusCode, 401); + }); + + await test("token verification failure", async () => { + const response = await fastify.inject({ + method: "GET", + url: "/secret", + headers: { + Authorization: + "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aWQiOiJjOTE3ZjNlMi05MzIyLTQ5MjYtOWJiMy1kYWNhNzMwNDEzY2EiLCJlbWFpbCI6InRlc3RAdXN0LmhrIiwibmFtZSI6IlRlc3QifQ.invalidsig", + }, + }); + // Unauthorized + assert.equal(response.statusCode, 401); + }); }); await suite("auth plugin with skipping", async () => { @@ -93,16 +103,12 @@ await suite("auth plugin with skipping", async () => { beforeEach(async () => { fastify = Fastify(); - await fastify.register(Auth, { - authDiscoveryURL: "", - authClientID: "", - authSkip: true, + await fastify.register(async function (fastify) { + await auth(fastify, { + authSkip: true, + }); + fastify.get("/secret", async () => "ok"); }); - fastify.get( - "/secret", - { preHandler: fastify.authPlugin }, - async () => "ok", - ); await fastify.ready(); }); afterEach(async () => { @@ -154,19 +160,4 @@ await suite("auth plugin with skipping", async () => { assert.equal(response.statusCode, 200); assert.equal(response.payload, "ok"); }); - - const token = process.env.TEST_AUTH_TOKEN; - - await test("valid token", { skip: token === undefined }, async () => { - const response = await fastify.inject({ - method: "GET", - url: "/secret", - headers: { - Authorization: `Bearer ${token}`, - }, - }); - // OK - assert.equal(response.statusCode, 200); - assert.equal(response.payload, "ok"); - }); }); diff --git a/test/routes/auth-example.test.ts b/test/routes/auth-example.test.ts new file mode 100644 index 0000000..77ffc76 --- /dev/null +++ b/test/routes/auth-example.test.ts @@ -0,0 +1,20 @@ +import { build } from "../helper.js"; +import * as assert from "node:assert"; +import { test } from "node:test"; + +test("auth-example routes are protected without leaking to public routes", async (t) => { + const app = await build(t, { + authSkip: false, + }); + + const protectedRes = await app.inject({ + url: "/auth-example", + }); + assert.equal(protectedRes.statusCode, 401); + + const publicRes = await app.inject({ + url: "/example", + }); + assert.equal(publicRes.statusCode, 200); + assert.equal(publicRes.payload, "this is an example"); +}); diff --git a/yarn.lock b/yarn.lock index 572c254..59b7d29 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1065,26 +1065,6 @@ __metadata: languageName: node linkType: hard -"@types/jsonwebtoken@npm:^9.0.10": - version: 9.0.10 - resolution: "@types/jsonwebtoken@npm:9.0.10" - dependencies: - "@types/ms": "npm:*" - "@types/node": "npm:*" - checksum: 10c0/0688ac8fb75f809201cb7e18a12b9d80ce539cb9dd27e1b01e11807cb1a337059e899b8ee3abc3f2c9417f02e363a3069d9eab9ef9724b1da1f0e10713514f94 - languageName: node - linkType: hard - -"@types/jsonwebtoken@npm:^9.0.4": - version: 9.0.9 - resolution: "@types/jsonwebtoken@npm:9.0.9" - dependencies: - "@types/ms": "npm:*" - "@types/node": "npm:*" - checksum: 10c0/d754a7b65fc021b298fc94e8d7a7d71f35dedf24296ac89286f80290abc5dbb0c7830a21440ee9ecbb340efc1b0a21f5609ea298a35b874cae5ad29a65440741 - languageName: node - linkType: hard - "@types/mdast@npm:^4.0.0": version: 4.0.4 resolution: "@types/mdast@npm:4.0.4" @@ -1101,15 +1081,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*": - version: 22.15.30 - resolution: "@types/node@npm:22.15.30" - dependencies: - undici-types: "npm:~6.21.0" - checksum: 10c0/ca330ac0e7fd502686d6df115fcc606aba46fd334220f749bbba2f639accdadcb23f7900603ceccdc8240be736739cad5c0b87c0fa92c9255a4dff245f07d664 - languageName: node - linkType: hard - "@types/node@npm:^25.3.0": version: 25.3.0 resolution: "@types/node@npm:25.3.0" @@ -1629,13 +1600,6 @@ __metadata: languageName: node linkType: hard -"buffer-equal-constant-time@npm:^1.0.1": - version: 1.0.1 - resolution: "buffer-equal-constant-time@npm:1.0.1" - checksum: 10c0/fb2294e64d23c573d0dd1f1e7a466c3e978fe94a4e0f8183937912ca374619773bef8e2aceb854129d2efecbbc515bbd0cc78d2734a3e3031edb0888531bbc8e - languageName: node - linkType: hard - "cacache@npm:^20.0.1": version: 20.0.3 resolution: "cacache@npm:20.0.3" @@ -1991,15 +1955,6 @@ __metadata: languageName: node linkType: hard -"ecdsa-sig-formatter@npm:1.0.11": - version: 1.0.11 - resolution: "ecdsa-sig-formatter@npm:1.0.11" - dependencies: - safe-buffer: "npm:^5.0.1" - checksum: 10c0/ebfbf19d4b8be938f4dd4a83b8788385da353d63307ede301a9252f9f7f88672e76f2191618fd8edfc2f24679236064176fab0b78131b161ee73daa37125408c - languageName: node - linkType: hard - "emoji-regex@npm:^10.3.0": version: 10.6.0 resolution: "emoji-regex@npm:10.6.0" @@ -3049,17 +3004,10 @@ __metadata: languageName: node linkType: hard -"jose@npm:^4.15.4": - version: 4.15.9 - resolution: "jose@npm:4.15.9" - checksum: 10c0/4ed4ddf4a029db04bd167f2215f65d7245e4dc5f36d7ac3c0126aab38d66309a9e692f52df88975d99429e357e5fd8bab340ff20baab544d17684dd1d940a0f4 - languageName: node - linkType: hard - -"jose@npm:^6.1.3": - version: 6.1.3 - resolution: "jose@npm:6.1.3" - checksum: 10c0/b9577b4a7a5e84131011c23823db9f5951eae3ba796771a6a2401ae5dd50daf71104febc8ded9c38146aa5ebe94a92ac09c725e699e613ef26949b9f5a8bc30f +"jose@npm:^6.2.2": + version: 6.2.2 + resolution: "jose@npm:6.2.2" + checksum: 10c0/201f4776d77eccd339de99fb3ba940fdf03db15e64be7a99b511e53c232e3f3818e3f21b95223d62f99315a2ab76b4251cedd94e067de56893e45273a8d2151b languageName: node linkType: hard @@ -3159,58 +3107,6 @@ __metadata: languageName: node linkType: hard -"jsonwebtoken@npm:^9.0.3": - version: 9.0.3 - resolution: "jsonwebtoken@npm:9.0.3" - dependencies: - jws: "npm:^4.0.1" - lodash.includes: "npm:^4.3.0" - lodash.isboolean: "npm:^3.0.3" - lodash.isinteger: "npm:^4.0.4" - lodash.isnumber: "npm:^3.0.3" - lodash.isplainobject: "npm:^4.0.6" - lodash.isstring: "npm:^4.0.1" - lodash.once: "npm:^4.0.0" - ms: "npm:^2.1.1" - semver: "npm:^7.5.4" - checksum: 10c0/6ca7f1e54886ea3bde7146a5a22b53847c46e25453c7f7307a69818b9a6ad48c390b2e59d5690fcfd03c529b01960060cc4bb0c686991d6edae2285dfd30f4ba - languageName: node - linkType: hard - -"jwa@npm:^2.0.1": - version: 2.0.1 - resolution: "jwa@npm:2.0.1" - dependencies: - buffer-equal-constant-time: "npm:^1.0.1" - ecdsa-sig-formatter: "npm:1.0.11" - safe-buffer: "npm:^5.0.1" - checksum: 10c0/ab3ebc6598e10dc11419d4ed675c9ca714a387481466b10e8a6f3f65d8d9c9237e2826f2505280a739cf4cbcf511cb288eeec22b5c9c63286fc5a2e4f97e78cf - languageName: node - linkType: hard - -"jwks-rsa@npm:^3.2.2": - version: 3.2.2 - resolution: "jwks-rsa@npm:3.2.2" - dependencies: - "@types/jsonwebtoken": "npm:^9.0.4" - debug: "npm:^4.3.4" - jose: "npm:^4.15.4" - limiter: "npm:^1.1.5" - lru-memoizer: "npm:^2.2.0" - checksum: 10c0/a619ee0c5b342e1c9edcd69086d7beb86e3ef1dcce5b8aeb3ac6d0c0014309c90f00e5e7b0ce8143832628244fec8cdff51ecc38a5c3a95a4fcd810d60e79b1a - languageName: node - linkType: hard - -"jws@npm:^4.0.1": - version: 4.0.1 - resolution: "jws@npm:4.0.1" - dependencies: - jwa: "npm:^2.0.1" - safe-buffer: "npm:^5.0.1" - checksum: 10c0/6be1ed93023aef570ccc5ea8d162b065840f3ef12f0d1bb3114cade844de7a357d5dc558201d9a65101e70885a6fa56b17462f520e6b0d426195510618a154d0 - languageName: node - linkType: hard - "keyv@npm:^4.5.4": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -3248,13 +3144,6 @@ __metadata: languageName: node linkType: hard -"limiter@npm:^1.1.5": - version: 1.1.5 - resolution: "limiter@npm:1.1.5" - checksum: 10c0/ebe2b20a820d1f67b8e1724051246434c419b2da041a7e9cd943f6daf113b8d17a52a1bd88fb79be5b624c10283ecb737f50edb5c1c88c71f4cd367108c97300 - languageName: node - linkType: hard - "lines-and-columns@npm:^1.1.6": version: 1.2.4 resolution: "lines-and-columns@npm:1.2.4" @@ -3335,55 +3224,6 @@ __metadata: languageName: node linkType: hard -"lodash.clonedeep@npm:^4.5.0": - version: 4.5.0 - resolution: "lodash.clonedeep@npm:4.5.0" - checksum: 10c0/2caf0e4808f319d761d2939ee0642fa6867a4bbf2cfce43276698828380756b99d4c4fa226d881655e6ac298dd453fe12a5ec8ba49861777759494c534936985 - languageName: node - linkType: hard - -"lodash.includes@npm:^4.3.0": - version: 4.3.0 - resolution: "lodash.includes@npm:4.3.0" - checksum: 10c0/7ca498b9b75bf602d04e48c0adb842dfc7d90f77bcb2a91a2b2be34a723ad24bc1c8b3683ec6b2552a90f216c723cdea530ddb11a3320e08fa38265703978f4b - languageName: node - linkType: hard - -"lodash.isboolean@npm:^3.0.3": - version: 3.0.3 - resolution: "lodash.isboolean@npm:3.0.3" - checksum: 10c0/0aac604c1ef7e72f9a6b798e5b676606042401dd58e49f051df3cc1e3adb497b3d7695635a5cbec4ae5f66456b951fdabe7d6b387055f13267cde521f10ec7f7 - languageName: node - linkType: hard - -"lodash.isinteger@npm:^4.0.4": - version: 4.0.4 - resolution: "lodash.isinteger@npm:4.0.4" - checksum: 10c0/4c3e023a2373bf65bf366d3b8605b97ec830bca702a926939bcaa53f8e02789b6a176e7f166b082f9365bfec4121bfeb52e86e9040cb8d450e64c858583f61b7 - languageName: node - linkType: hard - -"lodash.isnumber@npm:^3.0.3": - version: 3.0.3 - resolution: "lodash.isnumber@npm:3.0.3" - checksum: 10c0/2d01530513a1ee4f72dd79528444db4e6360588adcb0e2ff663db2b3f642d4bb3d687051ae1115751ca9082db4fdef675160071226ca6bbf5f0c123dbf0aa12d - languageName: node - linkType: hard - -"lodash.isplainobject@npm:^4.0.6": - version: 4.0.6 - resolution: "lodash.isplainobject@npm:4.0.6" - checksum: 10c0/afd70b5c450d1e09f32a737bed06ff85b873ecd3d3d3400458725283e3f2e0bb6bf48e67dbe7a309eb371a822b16a26cca4a63c8c52db3fc7dc9d5f9dd324cbb - languageName: node - linkType: hard - -"lodash.isstring@npm:^4.0.1": - version: 4.0.1 - resolution: "lodash.isstring@npm:4.0.1" - checksum: 10c0/09eaf980a283f9eef58ef95b30ec7fee61df4d6bf4aba3b5f096869cc58f24c9da17900febc8ffd67819b4e29de29793190e88dc96983db92d84c95fa85d1c92 - languageName: node - linkType: hard - "lodash.kebabcase@npm:^4.1.1": version: 4.1.1 resolution: "lodash.kebabcase@npm:4.1.1" @@ -3405,13 +3245,6 @@ __metadata: languageName: node linkType: hard -"lodash.once@npm:^4.0.0": - version: 4.1.1 - resolution: "lodash.once@npm:4.1.1" - checksum: 10c0/46a9a0a66c45dd812fcc016e46605d85ad599fe87d71a02f6736220554b52ffbe82e79a483ad40f52a8a95755b0d1077fba259da8bfb6694a7abbf4a48f1fc04 - languageName: node - linkType: hard - "lodash.snakecase@npm:^4.1.1": version: 4.1.1 resolution: "lodash.snakecase@npm:4.1.1" @@ -3446,15 +3279,6 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:6.0.0": - version: 6.0.0 - resolution: "lru-cache@npm:6.0.0" - dependencies: - yallist: "npm:^4.0.0" - checksum: 10c0/cb53e582785c48187d7a188d3379c181b5ca2a9c78d2bce3e7dee36f32761d1c42983da3fe12b55cb74e1779fa94cdc2e5367c028a9b35317184ede0c07a30a9 - languageName: node - linkType: hard - "lru-cache@npm:^11.0.0": version: 11.0.2 resolution: "lru-cache@npm:11.0.2" @@ -3469,16 +3293,6 @@ __metadata: languageName: node linkType: hard -"lru-memoizer@npm:^2.2.0": - version: 2.3.0 - resolution: "lru-memoizer@npm:2.3.0" - dependencies: - lodash.clonedeep: "npm:^4.5.0" - lru-cache: "npm:6.0.0" - checksum: 10c0/13cf6bc9ff74cdb167078dbb66d4cf43adc802495da8f56097e6f388b4d7ccb91668beb809bdbc55b62d016c138d7c19a18c5883a2fdbcc7f508ad8a23ec7c65 - languageName: node - linkType: hard - "make-dir@npm:^3.0.2": version: 3.1.0 resolution: "make-dir@npm:3.1.0" @@ -4058,7 +3872,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:^2.1.1, ms@npm:^2.1.3": +"ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 @@ -4135,13 +3949,6 @@ __metadata: languageName: node linkType: hard -"oauth4webapi@npm:^3.8.4": - version: 3.8.4 - resolution: "oauth4webapi@npm:3.8.4" - checksum: 10c0/961d82ecb44b994b116616f7f60b2c6ba03dfccb36815e4b8976597e98da1938521ed6465807d6941029ea69488ea146de2bf210eb69a435c109e18a6f46ec80 - languageName: node - linkType: hard - "on-exit-leak-free@npm:^2.1.0": version: 2.1.2 resolution: "on-exit-leak-free@npm:2.1.2" @@ -4174,16 +3981,6 @@ __metadata: languageName: node linkType: hard -"openid-client@npm:^6.8.2": - version: 6.8.2 - resolution: "openid-client@npm:6.8.2" - dependencies: - jose: "npm:^6.1.3" - oauth4webapi: "npm:^3.8.4" - checksum: 10c0/98de93a1d1135f7c51b7707258ec0506bc6dc5359acf06f922cd4258635d657d1a80b740ad4a11ae9e79ce6061dd0d19cf5115a9918866f0b46c5486cfbbf22c - languageName: node - linkType: hard - "optionator@npm:^0.9.3": version: 0.9.4 resolution: "optionator@npm:0.9.4" @@ -4720,7 +4517,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 @@ -4782,15 +4579,6 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.5.4": - version: 7.7.2 - resolution: "semver@npm:7.7.2" - bin: - semver: bin/semver.js - checksum: 10c0/aca305edfbf2383c22571cb7714f48cadc7ac95371b4b52362fb8eeffdfbc0de0669368b82b2b15978f8848f01d7114da65697e56cd8c37b0dab8c58e543f9ea - languageName: node - linkType: hard - "semver@npm:^7.7.3": version: 7.7.4 resolution: "semver@npm:7.7.4" @@ -5074,7 +4862,6 @@ __metadata: "@fastify/type-provider-typebox": "npm:^6.1.0" "@scalar/fastify-api-reference": "npm:^1.44.24" "@trivago/prettier-plugin-sort-imports": "npm:^6.0.2" - "@types/jsonwebtoken": "npm:^9.0.10" "@types/node": "npm:^25.3.0" "@typescript/native-preview": "npm:^7.0.0-dev.20260219.1" eslint: "npm:^9.39.2" @@ -5084,11 +4871,9 @@ __metadata: fastify-metrics: "npm:^12.1.0" fastify-plugin: "npm:^5.1.0" husky: "npm:^9.1.7" - jsonwebtoken: "npm:^9.0.3" - jwks-rsa: "npm:^3.2.2" + jose: "npm:^6.2.2" lint-staged: "npm:^16.2.7" mongodb-memory-server: "npm:11.0.1" - openid-client: "npm:^6.8.2" pino-loki: "npm:^3.0.0" prettier: "npm:^3.8.1" prettier-plugin-jsdoc: "npm:^1.8.0" @@ -5286,13 +5071,6 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~6.21.0": - version: 6.21.0 - resolution: "undici-types@npm:6.21.0" - checksum: 10c0/c01ed51829b10aa72fc3ce64b747f8e74ae9b60eafa19a7b46ef624403508a54c526ffab06a14a26b3120d055e1104d7abe7c9017e83ced038ea5cf52f8d5e04 - languageName: node - linkType: hard - "undici-types@npm:~7.18.0": version: 7.18.2 resolution: "undici-types@npm:7.18.2"