better-auth
Version:
The most comprehensive authentication framework for TypeScript.
1 lines • 9.33 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/plugins/one-time-token/index.ts"],"sourcesContent":["import type {\n\tBetterAuthPlugin,\n\tGenericEndpointContext,\n} from \"@better-auth/core\";\nimport {\n\tcreateAuthEndpoint,\n\tcreateAuthMiddleware,\n} from \"@better-auth/core/api\";\nimport * as z from \"zod\";\nimport { sessionMiddleware } from \"../../api\";\nimport { setSessionCookie } from \"../../cookies\";\nimport { generateRandomString } from \"../../crypto\";\nimport type { Session, User } from \"../../types\";\nimport { defaultKeyHasher } from \"./utils\";\n\nexport interface OneTimeTokenOptions {\n\t/**\n\t * Expires in minutes\n\t *\n\t * @default 3\n\t */\n\texpiresIn?: number | undefined;\n\t/**\n\t * Only allow server initiated requests\n\t */\n\tdisableClientRequest?: boolean | undefined;\n\t/**\n\t * Generate a custom token\n\t */\n\tgenerateToken?:\n\t\t| ((\n\t\t\t\tsession: {\n\t\t\t\t\tuser: User & Record<string, any>;\n\t\t\t\t\tsession: Session & Record<string, any>;\n\t\t\t\t},\n\t\t\t\tctx: GenericEndpointContext,\n\t\t ) => Promise<string>)\n\t\t| undefined;\n\t/**\n\t * Disable setting the session cookie when the token is verified\n\t */\n\tdisableSetSessionCookie?: boolean;\n\t/**\n\t * This option allows you to configure how the token is stored in your database.\n\t * Note: This will not affect the token that's sent, it will only affect the token stored in your database.\n\t *\n\t * @default \"plain\"\n\t */\n\tstoreToken?:\n\t\t| (\n\t\t\t\t| \"plain\"\n\t\t\t\t| \"hashed\"\n\t\t\t\t| { type: \"custom-hasher\"; hash: (token: string) => Promise<string> }\n\t\t )\n\t\t| undefined;\n\t/**\n\t * Set the OTT header on new sessions\n\t */\n\tsetOttHeaderOnNewSession?: boolean;\n}\n\nconst verifyOneTimeTokenBodySchema = z.object({\n\ttoken: z.string().meta({\n\t\tdescription: 'The token to verify. Eg: \"some-token\"',\n\t}),\n});\n\nexport const oneTimeToken = (options?: OneTimeTokenOptions | undefined) => {\n\tconst opts = {\n\t\tstoreToken: \"plain\",\n\t\t...options,\n\t} satisfies OneTimeTokenOptions;\n\n\tasync function storeToken(ctx: GenericEndpointContext, token: string) {\n\t\tif (opts.storeToken === \"hashed\") {\n\t\t\treturn await defaultKeyHasher(token);\n\t\t}\n\t\tif (\n\t\t\ttypeof opts.storeToken === \"object\" &&\n\t\t\t\"type\" in opts.storeToken &&\n\t\t\topts.storeToken.type === \"custom-hasher\"\n\t\t) {\n\t\t\treturn await opts.storeToken.hash(token);\n\t\t}\n\n\t\treturn token;\n\t}\n\n\tasync function generateToken(\n\t\tc: GenericEndpointContext,\n\t\tsession: {\n\t\t\tsession: Session;\n\t\t\tuser: User;\n\t\t},\n\t) {\n\t\tconst token = opts?.generateToken\n\t\t\t? await opts.generateToken(session, c)\n\t\t\t: generateRandomString(32);\n\t\tconst expiresAt = new Date(Date.now() + (opts?.expiresIn ?? 3) * 60 * 1000);\n\t\tconst storedToken = await storeToken(c, token);\n\t\tawait c.context.internalAdapter.createVerificationValue({\n\t\t\tvalue: session.session.token,\n\t\t\tidentifier: `one-time-token:${storedToken}`,\n\t\t\texpiresAt,\n\t\t});\n\t\treturn token;\n\t}\n\n\treturn {\n\t\tid: \"one-time-token\",\n\t\tendpoints: {\n\t\t\t/**\n\t\t\t * ### Endpoint\n\t\t\t *\n\t\t\t * GET `/one-time-token/generate`\n\t\t\t *\n\t\t\t * ### API Methods\n\t\t\t *\n\t\t\t * **server:**\n\t\t\t * `auth.api.generateOneTimeToken`\n\t\t\t *\n\t\t\t * **client:**\n\t\t\t * `authClient.oneTimeToken.generate`\n\t\t\t *\n\t\t\t * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/one-time-token#api-method-one-time-token-generate)\n\t\t\t */\n\t\t\tgenerateOneTimeToken: createAuthEndpoint(\n\t\t\t\t\"/one-time-token/generate\",\n\t\t\t\t{\n\t\t\t\t\tmethod: \"GET\",\n\t\t\t\t\tuse: [sessionMiddleware],\n\t\t\t\t},\n\t\t\t\tasync (c) => {\n\t\t\t\t\t//if request exist, it means it's a client request\n\t\t\t\t\tif (opts?.disableClientRequest && c.request) {\n\t\t\t\t\t\tthrow c.error(\"BAD_REQUEST\", {\n\t\t\t\t\t\t\tmessage: \"Client requests are disabled\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tconst session = c.context.session;\n\t\t\t\t\tconst token = await generateToken(c, session);\n\t\t\t\t\treturn c.json({ token });\n\t\t\t\t},\n\t\t\t),\n\t\t\t/**\n\t\t\t * ### Endpoint\n\t\t\t *\n\t\t\t * POST `/one-time-token/verify`\n\t\t\t *\n\t\t\t * ### API Methods\n\t\t\t *\n\t\t\t * **server:**\n\t\t\t * `auth.api.verifyOneTimeToken`\n\t\t\t *\n\t\t\t * **client:**\n\t\t\t * `authClient.oneTimeToken.verify`\n\t\t\t *\n\t\t\t * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/one-time-token#api-method-one-time-token-verify)\n\t\t\t */\n\t\t\tverifyOneTimeToken: createAuthEndpoint(\n\t\t\t\t\"/one-time-token/verify\",\n\t\t\t\t{\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\tbody: verifyOneTimeTokenBodySchema,\n\t\t\t\t},\n\t\t\t\tasync (c) => {\n\t\t\t\t\tconst { token } = c.body;\n\t\t\t\t\tconst storedToken = await storeToken(c, token);\n\t\t\t\t\tconst verificationValue =\n\t\t\t\t\t\tawait c.context.internalAdapter.findVerificationValue(\n\t\t\t\t\t\t\t`one-time-token:${storedToken}`,\n\t\t\t\t\t\t);\n\t\t\t\t\tif (!verificationValue) {\n\t\t\t\t\t\tthrow c.error(\"BAD_REQUEST\", {\n\t\t\t\t\t\t\tmessage: \"Invalid token\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tawait c.context.internalAdapter.deleteVerificationValue(\n\t\t\t\t\t\tverificationValue.id,\n\t\t\t\t\t);\n\t\t\t\t\tif (verificationValue.expiresAt < new Date()) {\n\t\t\t\t\t\tthrow c.error(\"BAD_REQUEST\", {\n\t\t\t\t\t\t\tmessage: \"Token expired\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tconst session = await c.context.internalAdapter.findSession(\n\t\t\t\t\t\tverificationValue.value,\n\t\t\t\t\t);\n\t\t\t\t\tif (!session) {\n\t\t\t\t\t\tthrow c.error(\"BAD_REQUEST\", {\n\t\t\t\t\t\t\tmessage: \"Session not found\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tif (!opts?.disableSetSessionCookie) {\n\t\t\t\t\t\tawait setSessionCookie(c, session);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (session.session.expiresAt < new Date()) {\n\t\t\t\t\t\tthrow c.error(\"BAD_REQUEST\", {\n\t\t\t\t\t\t\tmessage: \"Session expired\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\treturn c.json(session);\n\t\t\t\t},\n\t\t\t),\n\t\t},\n\t\thooks: {\n\t\t\tafter: [\n\t\t\t\t{\n\t\t\t\t\tmatcher: () => true,\n\t\t\t\t\thandler: createAuthMiddleware(async (ctx) => {\n\t\t\t\t\t\tif (ctx.context.newSession) {\n\t\t\t\t\t\t\tif (!opts?.setOttHeaderOnNewSession) {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconst exposedHeaders =\n\t\t\t\t\t\t\t\tctx.context.responseHeaders?.get(\n\t\t\t\t\t\t\t\t\t\"access-control-expose-headers\",\n\t\t\t\t\t\t\t\t) || \"\";\n\t\t\t\t\t\t\tconst headersSet = new Set(\n\t\t\t\t\t\t\t\texposedHeaders\n\t\t\t\t\t\t\t\t\t.split(\",\")\n\t\t\t\t\t\t\t\t\t.map((header) => header.trim())\n\t\t\t\t\t\t\t\t\t.filter(Boolean),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\theadersSet.add(\"set-ott\");\n\t\t\t\t\t\t\tconst token = await generateToken(ctx, ctx.context.newSession);\n\t\t\t\t\t\t\tctx.setHeader(\"set-ott\", token);\n\t\t\t\t\t\t\tctx.setHeader(\n\t\t\t\t\t\t\t\t\"Access-Control-Expose-Headers\",\n\t\t\t\t\t\t\t\tArray.from(headersSet).join(\", \"),\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],\n\t\t},\n\t\toptions,\n\t} satisfies BetterAuthPlugin;\n};\n"],"mappings":"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"}