better-auth
Version:
The most comprehensive authentication framework for TypeScript.
1 lines • 15.3 kB
Source Map (JSON)
{"version":3,"file":"verify-api-key.mjs","names":["updated: ApiKey","newApiKey: ApiKey | null","apiKey: ApiKey | null"],"sources":["../../../../src/plugins/api-key/routes/verify-api-key.ts"],"sourcesContent":["import type { AuthContext, GenericEndpointContext } from \"@better-auth/core\";\nimport { createAuthEndpoint } from \"@better-auth/core/api\";\nimport { safeJSONParse } from \"@better-auth/core/utils\";\nimport * as z from \"zod\";\nimport { APIError } from \"../../../api\";\nimport { role } from \"../../access\";\nimport { API_KEY_TABLE_NAME, ERROR_CODES } from \"..\";\nimport { defaultKeyHasher } from \"../\";\nimport { deleteApiKey, getApiKey, setApiKey } from \"../adapter\";\nimport { isRateLimited } from \"../rate-limit\";\nimport type { apiKeySchema } from \"../schema\";\nimport type { ApiKey } from \"../types\";\nimport type { PredefinedApiKeyOptions } from \".\";\n\nexport async function validateApiKey({\n\thashedKey,\n\tctx,\n\topts,\n\tschema,\n\tpermissions,\n}: {\n\thashedKey: string;\n\topts: PredefinedApiKeyOptions;\n\tschema: ReturnType<typeof apiKeySchema>;\n\tpermissions?: Record<string, string[]> | undefined;\n\tctx: GenericEndpointContext;\n}) {\n\tconst apiKey = await getApiKey(ctx, hashedKey, opts);\n\n\tif (!apiKey) {\n\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\tmessage: ERROR_CODES.INVALID_API_KEY,\n\t\t});\n\t}\n\n\tif (apiKey.enabled === false) {\n\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\tmessage: ERROR_CODES.KEY_DISABLED,\n\t\t\tcode: \"KEY_DISABLED\" as const,\n\t\t});\n\t}\n\n\tif (apiKey.expiresAt) {\n\t\tconst now = Date.now();\n\t\tconst expiresAt = new Date(apiKey.expiresAt).getTime();\n\t\tif (now > expiresAt) {\n\t\t\tconst deleteExpiredKey = async () => {\n\t\t\t\tif (opts.storage === \"secondary-storage\" && opts.fallbackToDatabase) {\n\t\t\t\t\tawait deleteApiKey(ctx, apiKey, opts);\n\t\t\t\t\tawait ctx.context.adapter.delete({\n\t\t\t\t\t\tmodel: API_KEY_TABLE_NAME,\n\t\t\t\t\t\twhere: [{ field: \"id\", value: apiKey.id }],\n\t\t\t\t\t});\n\t\t\t\t} else if (opts.storage === \"secondary-storage\") {\n\t\t\t\t\tawait deleteApiKey(ctx, apiKey, opts);\n\t\t\t\t} else {\n\t\t\t\t\tawait ctx.context.adapter.delete({\n\t\t\t\t\t\tmodel: API_KEY_TABLE_NAME,\n\t\t\t\t\t\twhere: [{ field: \"id\", value: apiKey.id }],\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tif (opts.deferUpdates) {\n\t\t\t\tctx.context.runInBackground(\n\t\t\t\t\tdeleteExpiredKey().catch((error) => {\n\t\t\t\t\t\tctx.context.logger.error(\"Deferred update failed:\", error);\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tawait deleteExpiredKey();\n\t\t\t}\n\n\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\tmessage: ERROR_CODES.KEY_EXPIRED,\n\t\t\t\tcode: \"KEY_EXPIRED\" as const,\n\t\t\t});\n\t\t}\n\t}\n\n\tif (permissions) {\n\t\tconst apiKeyPermissions = apiKey.permissions\n\t\t\t? safeJSONParse<{\n\t\t\t\t\t[key: string]: string[];\n\t\t\t\t}>(apiKey.permissions)\n\t\t\t: null;\n\n\t\tif (!apiKeyPermissions) {\n\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\tmessage: ERROR_CODES.KEY_NOT_FOUND,\n\t\t\t\tcode: \"KEY_NOT_FOUND\" as const,\n\t\t\t});\n\t\t}\n\t\tconst r = role(apiKeyPermissions as any);\n\t\tconst result = r.authorize(permissions);\n\t\tif (!result.success) {\n\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\tmessage: ERROR_CODES.KEY_NOT_FOUND,\n\t\t\t\tcode: \"KEY_NOT_FOUND\" as const,\n\t\t\t});\n\t\t}\n\t}\n\n\tlet remaining = apiKey.remaining;\n\tlet lastRefillAt = apiKey.lastRefillAt;\n\n\tif (apiKey.remaining === 0 && apiKey.refillAmount === null) {\n\t\tconst deleteExhaustedKey = async () => {\n\t\t\tif (opts.storage === \"secondary-storage\" && opts.fallbackToDatabase) {\n\t\t\t\tawait deleteApiKey(ctx, apiKey, opts);\n\t\t\t\tawait ctx.context.adapter.delete({\n\t\t\t\t\tmodel: API_KEY_TABLE_NAME,\n\t\t\t\t\twhere: [{ field: \"id\", value: apiKey.id }],\n\t\t\t\t});\n\t\t\t} else if (opts.storage === \"secondary-storage\") {\n\t\t\t\tawait deleteApiKey(ctx, apiKey, opts);\n\t\t\t} else {\n\t\t\t\tawait ctx.context.adapter.delete({\n\t\t\t\t\tmodel: API_KEY_TABLE_NAME,\n\t\t\t\t\twhere: [{ field: \"id\", value: apiKey.id }],\n\t\t\t\t});\n\t\t\t}\n\t\t};\n\n\t\tif (opts.deferUpdates) {\n\t\t\tctx.context.runInBackground(\n\t\t\t\tdeleteExhaustedKey().catch((error) => {\n\t\t\t\t\tctx.context.logger.error(\"Deferred update failed:\", error);\n\t\t\t\t}),\n\t\t\t);\n\t\t} else {\n\t\t\tawait deleteExhaustedKey();\n\t\t}\n\n\t\tthrow new APIError(\"TOO_MANY_REQUESTS\", {\n\t\t\tmessage: ERROR_CODES.USAGE_EXCEEDED,\n\t\t\tcode: \"USAGE_EXCEEDED\" as const,\n\t\t});\n\t} else if (remaining !== null) {\n\t\tlet now = Date.now();\n\t\tconst refillInterval = apiKey.refillInterval;\n\t\tconst refillAmount = apiKey.refillAmount;\n\t\tlet lastTime = new Date(lastRefillAt ?? apiKey.createdAt).getTime();\n\n\t\tif (refillInterval && refillAmount) {\n\t\t\t// if they provide refill info, then we should refill once the interval is reached.\n\n\t\t\tconst timeSinceLastRequest = now - lastTime;\n\t\t\tif (timeSinceLastRequest > refillInterval) {\n\t\t\t\tremaining = refillAmount;\n\t\t\t\tlastRefillAt = new Date();\n\t\t\t}\n\t\t}\n\n\t\tif (remaining === 0) {\n\t\t\t// if there are no more remaining requests, than the key is invalid\n\t\t\tthrow new APIError(\"TOO_MANY_REQUESTS\", {\n\t\t\t\tmessage: ERROR_CODES.USAGE_EXCEEDED,\n\t\t\t\tcode: \"USAGE_EXCEEDED\" as const,\n\t\t\t});\n\t\t} else {\n\t\t\tremaining--;\n\t\t}\n\t}\n\n\tconst { message, success, update, tryAgainIn } = isRateLimited(apiKey, opts);\n\n\tif (success === false) {\n\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\tmessage: message ?? undefined,\n\t\t\tcode: \"RATE_LIMITED\" as const,\n\t\t\tdetails: {\n\t\t\t\ttryAgainIn,\n\t\t\t},\n\t\t});\n\t}\n\n\tconst updated: ApiKey = {\n\t\t...apiKey,\n\t\t...update,\n\t\tremaining,\n\t\tlastRefillAt,\n\t\tupdatedAt: new Date(),\n\t};\n\n\tconst performUpdate = async (): Promise<ApiKey | null> => {\n\t\tif (opts.storage === \"database\") {\n\t\t\treturn ctx.context.adapter.update<ApiKey>({\n\t\t\t\tmodel: API_KEY_TABLE_NAME,\n\t\t\t\twhere: [{ field: \"id\", value: apiKey.id }],\n\t\t\t\tupdate: { ...updated, id: undefined },\n\t\t\t});\n\t\t} else if (\n\t\t\topts.storage === \"secondary-storage\" &&\n\t\t\topts.fallbackToDatabase\n\t\t) {\n\t\t\tconst dbUpdated = await ctx.context.adapter.update<ApiKey>({\n\t\t\t\tmodel: API_KEY_TABLE_NAME,\n\t\t\t\twhere: [{ field: \"id\", value: apiKey.id }],\n\t\t\t\tupdate: { ...updated, id: undefined },\n\t\t\t});\n\t\t\tif (dbUpdated) {\n\t\t\t\tawait setApiKey(ctx, dbUpdated, opts);\n\t\t\t}\n\t\t\treturn dbUpdated;\n\t\t} else {\n\t\t\tawait setApiKey(ctx, updated, opts);\n\t\t\treturn updated;\n\t\t}\n\t};\n\n\tlet newApiKey: ApiKey | null = null;\n\n\tif (opts.deferUpdates) {\n\t\tctx.context.runInBackground(\n\t\t\tperformUpdate()\n\t\t\t\t.then(() => {})\n\t\t\t\t.catch((error) => {\n\t\t\t\t\tctx.context.logger.error(\"Failed to update API key:\", error);\n\t\t\t\t}),\n\t\t);\n\t\tnewApiKey = updated;\n\t} else {\n\t\tnewApiKey = await performUpdate();\n\t\tif (!newApiKey) {\n\t\t\tthrow new APIError(\"INTERNAL_SERVER_ERROR\", {\n\t\t\t\tmessage: ERROR_CODES.FAILED_TO_UPDATE_API_KEY,\n\t\t\t\tcode: \"INTERNAL_SERVER_ERROR\" as const,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn newApiKey;\n}\n\nconst verifyApiKeyBodySchema = z.object({\n\tkey: z.string().meta({\n\t\tdescription: \"The key to verify\",\n\t}),\n\tpermissions: z\n\t\t.record(z.string(), z.array(z.string()))\n\t\t.meta({\n\t\t\tdescription: \"The permissions to verify.\",\n\t\t})\n\t\t.optional(),\n});\n\nexport function verifyApiKey({\n\topts,\n\tschema,\n\tdeleteAllExpiredApiKeys,\n}: {\n\topts: PredefinedApiKeyOptions;\n\tschema: ReturnType<typeof apiKeySchema>;\n\tdeleteAllExpiredApiKeys(\n\t\tctx: AuthContext,\n\t\tbyPassLastCheckTime?: boolean | undefined,\n\t): Promise<void>;\n}) {\n\treturn createAuthEndpoint(\n\t\t{\n\t\t\tmethod: \"POST\",\n\t\t\tbody: verifyApiKeyBodySchema,\n\t\t},\n\t\tasync (ctx) => {\n\t\t\tconst { key } = ctx.body;\n\n\t\t\tif (key.length < opts.defaultKeyLength) {\n\t\t\t\t// if the key is shorter than the default key length, than we know the key is invalid.\n\t\t\t\t// we can't check if the key is exactly equal to the default key length, because\n\t\t\t\t// a prefix may be added to the key.\n\t\t\t\treturn ctx.json({\n\t\t\t\t\tvalid: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tmessage: ERROR_CODES.INVALID_API_KEY,\n\t\t\t\t\t\tcode: \"KEY_NOT_FOUND\" as const,\n\t\t\t\t\t},\n\t\t\t\t\tkey: null,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (opts.customAPIKeyValidator) {\n\t\t\t\tconst isValid = await opts.customAPIKeyValidator({ ctx, key });\n\t\t\t\tif (!isValid) {\n\t\t\t\t\treturn ctx.json({\n\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\terror: {\n\t\t\t\t\t\t\tmessage: ERROR_CODES.INVALID_API_KEY,\n\t\t\t\t\t\t\tcode: \"KEY_NOT_FOUND\" as const,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tkey: null,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst hashed = opts.disableKeyHashing ? key : await defaultKeyHasher(key);\n\n\t\t\tlet apiKey: ApiKey | null = null;\n\n\t\t\ttry {\n\t\t\t\tapiKey = await validateApiKey({\n\t\t\t\t\thashedKey: hashed,\n\t\t\t\t\tpermissions: ctx.body.permissions,\n\t\t\t\t\tctx,\n\t\t\t\t\topts,\n\t\t\t\t\tschema,\n\t\t\t\t});\n\n\t\t\t\tif (opts.deferUpdates) {\n\t\t\t\t\tctx.context.runInBackground(\n\t\t\t\t\t\tdeleteAllExpiredApiKeys(ctx.context).catch((err) => {\n\t\t\t\t\t\t\tctx.context.logger.error(\n\t\t\t\t\t\t\t\t\"Failed to delete expired API keys:\",\n\t\t\t\t\t\t\t\terr,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof APIError) {\n\t\t\t\t\treturn ctx.json({\n\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\terror: {\n\t\t\t\t\t\t\tmessage: error.body?.message,\n\t\t\t\t\t\t\tcode: error.body?.code as string,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tkey: null,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\treturn ctx.json({\n\t\t\t\t\tvalid: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tmessage: ERROR_CODES.INVALID_API_KEY,\n\t\t\t\t\t\tcode: \"INVALID_API_KEY\" as const,\n\t\t\t\t\t},\n\t\t\t\t\tkey: null,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst { key: _, ...returningApiKey } = apiKey ?? {\n\t\t\t\tkey: 1,\n\t\t\t\tpermissions: undefined,\n\t\t\t};\n\t\t\tif (\"metadata\" in returningApiKey) {\n\t\t\t\treturningApiKey.metadata =\n\t\t\t\t\tschema.apikey.fields.metadata.transform.output(\n\t\t\t\t\t\treturningApiKey.metadata as never as string,\n\t\t\t\t\t);\n\t\t\t}\n\n\t\t\treturningApiKey.permissions = returningApiKey.permissions\n\t\t\t\t? safeJSONParse<{\n\t\t\t\t\t\t[key: string]: string[];\n\t\t\t\t\t}>(returningApiKey.permissions)\n\t\t\t\t: null;\n\n\t\t\treturn ctx.json({\n\t\t\t\tvalid: true,\n\t\t\t\terror: null,\n\t\t\t\tkey: apiKey === null ? null : (returningApiKey as Omit<ApiKey, \"key\">),\n\t\t\t});\n\t\t},\n\t);\n}\n"],"mappings":"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"}