[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: pilcrowOnPaper\n"
  },
  {
    "path": ".github/workflows/publish.yaml",
    "content": "name: \"Publish\"\non:\n  push:\n    branches:\n      - main\n\nenv:\n  CLOUDFLARE_API_TOKEN: ${{secrets.CLOUDFLARE_PAGES_API_TOKEN}}\n\njobs:\n  publish:\n    name: Publish\n    runs-on: ubuntu-latest\n    steps:\n      - name: setup actions\n        uses: actions/checkout@v3\n      - name: setup node\n        uses: actions/setup-node@v3\n        with:\n          node-version: 20.5.1\n          registry-url: https://registry.npmjs.org\n      - name: install malta\n        run: |\n          curl -o malta.tgz -L https://github.com/pilcrowonpaper/malta/releases/latest/download/linux-amd64.tgz\n          tar -xvzf malta.tgz\n      - name: build\n        run: ./linux-amd64/malta build\n      - name: install wrangler\n        run: npm i -g wrangler\n      - name: deploy\n        run: wrangler pages deploy dist --project-name lucia --branch main\n"
  },
  {
    "path": ".gitignore",
    "content": "dist\npnpm-lock.yaml\nnode_modules\npackage-lock.json\n.DS_Store"
  },
  {
    "path": ".prettierignore",
    "content": ".DS_Store\nnode_modules\n/dist\n\npnpm-lock.yaml\npackage-lock.json\nyarn.lock\n\n"
  },
  {
    "path": ".prettierrc.json",
    "content": "{\n\t\"useTabs\": true,\n\t\"trailingComma\": \"none\",\n\t\"printWidth\": 100\n}\n"
  },
  {
    "path": "LICENSE-0BSD",
    "content": "Copyright (c) 2024 pilcrowOnPaper and contributors\n\nPermission to use, copy, modify, and/or distribute this software for\nany purpose with or without fee is hereby granted.\n\nTHE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL\nWARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES\nOF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE\nFOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY\nDAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN\nAN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\nOF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE."
  },
  {
    "path": "LICENSE-MIT",
    "content": "Copyright (c) 2024 pilcrowOnPaper and contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "# Lucia\n\n**Link: [lucia-auth.com](https://lucia-auth.com)**\n\n> [!IMPORTANT]  \n> Lucia v3 will be deprecated by March 2025. Lucia is now a learning resource on implementing auth from scratch. See the [announcement](https://github.com/lucia-auth/lucia/discussions/1714) for details and migration path. The source code for v3 is available in the `v3` branch.\n\nLucia is an open source project to provide resources on implementing authentication with JavaScript and TypeScript.\n\nThe main section is on implementing sessions with your database, library, and framework of choice. Using the API you just created, you can continue learning by going through the tutorials or by referencing one of the fully-fledged examples.\n\nIf you have any questions on auth, feel free to ask them in our [Discord server](https://discord.com/invite/PwrK3kpVR3) or on [GitHub Discussions](https://github.com/lucia-auth/lucia/discussions)!\n\n## Why not a library?\n\nWe've found it extremely hard to develop a library that:\n\n1. Supports the many database libraries, ORMs, frameworks, runtimes, and deployment options available in the ecosystem.\n2. Provides enough flexibility for the majority of use cases.\n3. Does not add significant complexity to projects.\n\nWe came to the conclusion that at least for the core of auth - sessions - it's better to teach the code and concepts rather than to try cramming it into a library. The code is very straightforward and shouldn't take more than 10 minutes to write it once you understand it. As an added bonus, it's fully customizable.\n\n## Related projects\n\n- [The Copenhagen Book](https://thecopenhagenbook.com): A free online resource covering the various auth concepts in web applications.\n- [Oslo](https://oslojs.dev): Simple, runtime agnostic, and fully-typed packages with minimal dependency for auth and cryptography.\n- [Arctic](https://arcticjs.dev): OAuth 2.0 client library with support for 50+ providers.\n\n## Disclaimer\n\nAll example code in the site is licensed under the [Zero-Clause BSD license](https://github.com/lucia-auth/lucia/blob/main/LICENSE-0BSD). You're free to use, copy, modify, and distribute it without any attribution. The license is approved by the [Open Source Initiative (OSI)](https://opensource.org/license/0bsd) and [Google](https://opensource.google/documentation/reference/patching#forbidden).\n\nEverything else this repository is licensed under the [MIT license](https://github.com/lucia-auth/lucia/blob/main/LICENSE-MIT).\n\n_Copyright © 2024 pilcrow and contributors_\n"
  },
  {
    "path": "malta.config.json",
    "content": "{\n\t\"name\": \"Lucia\",\n\t\"description\": \"An open source resource on implementing authentication with JavaScript\",\n\t\"domain\": \"https://lucia-auth.com\",\n\t\"twitter\": \"@lucia_auth\",\n\t\"asset_hashing\": true,\n\t\"sidebar\": [\n\t\t{\n\t\t\t\"title\": \"Sessions\",\n\t\t\t\"pages\": [\n\t\t\t\t[\"Overview\", \"/sessions/overview\"],\n\t\t\t\t[\"Basic implementation\", \"/sessions/basic\"],\n\t\t\t\t[\"Inactivity timeout\", \"/sessions/inactivity-timeout\"],\n\t\t\t\t[\"Stateless tokens\", \"/sessions/stateless-tokens\"],\n\t\t\t\t[\"Frameworks\", \"/sessions/frameworks\"]\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"title\": \"Tutorials\",\n\t\t\t\"pages\": [\n\t\t\t\t[\"GitHub OAuth\", \"/tutorials/github-oauth\"],\n\t\t\t\t[\"Google OAuth\", \"/tutorials/google-oauth\"]\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"title\": \"Example projects\",\n\t\t\t\"pages\": [\n\t\t\t\t[\"GitHub OAuth\", \"/examples/github-oauth\"],\n\t\t\t\t[\"Google OAuth\", \"/examples/google-oauth\"],\n\t\t\t\t[\"Email and password with 2FA\", \"/examples/email-password-2fa\"],\n\t\t\t\t[\"Email and password with 2FA and WebAuthn\", \"/examples/email-password-2fa-webauthn\"]\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"title\": \"Rate limiting\",\n\t\t\t\"pages\": [[\"Token bucket\", \"/rate-limit/token-bucket\"]]\n\t\t},\n\t\t{\n\t\t\t\"title\": \"Lucia v3\",\n\t\t\t\"pages\": [[\"Migrate\", \"/lucia-v3/migrate\"]]\n\t\t},\n\t\t{\n\t\t\t\"title\": \"Community\",\n\t\t\t\"pages\": [\n\t\t\t\t[\"GitHub\", \"https://github.com/lucia-auth/lucia\"],\n\t\t\t\t[\"Discord\", \"https://discord.com/invite/PwrK3kpVR3\"],\n\t\t\t\t[\"Twitter\", \"https://x.com/lucia_auth\"],\n\t\t\t\t[\"Donate\", \"https://github.com/sponsors/pilcrowOnPaper\"]\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"title\": \"Related projects\",\n\t\t\t\"pages\": [\n\t\t\t\t[\"The Copenhagen Book\", \"https://thecopenhagenbook.com\"],\n\t\t\t\t[\"Oslo\", \"https://oslojs.dev\"],\n\t\t\t\t[\"Arctic\", \"https://arcticjs.dev\"]\n\t\t\t]\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"lucia\",\n\t\"scripts\": {\n\t\t\"format\": \"prettier -w .\"\n\t},\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"https://github.com/lucia-auth/lucia\"\n\t},\n\t\"author\": \"pilcrowOnPaper\",\n\t\"license\": \"MIT\",\n\t\"devDependencies\": {\n\t\t\"prettier\": \"^3.0.3\"\n\t}\n}\n"
  },
  {
    "path": "pages/examples/email-password-2fa-webauthn.md",
    "content": "---\ntitle: \"Email and password with 2FA and WebAuthn\"\n---\n\n# Email and password with 2FA and WebAuthn\n\nExample project with:\n\n- Email and password authentication\n- Password checks with HaveIBeenPwned\n- Sign in with passkeys\n- Email verification\n- 2FA with TOTP\n- 2FA recovery codes\n- 2FA with passkeys and security keys\n- Password reset with 2FA\n- Login throttling and rate limiting\n\n## GitHub repositories\n\n- [Astro](https://github.com/lucia-auth/example-astro-email-password-webauthn)\n- [Next.js](https://github.com/lucia-auth/example-nextjs-email-password-webauthn)\n- [SvelteKit](https://github.com/lucia-auth/example-sveltekit-email-password-webauthn)\n"
  },
  {
    "path": "pages/examples/email-password-2fa.md",
    "content": "---\ntitle: \"Email and password with 2FA\"\n---\n\n# Email and password with 2FA\n\nExample project with:\n\n- Email and password authentication\n- Password check with HaveIBeenPwned\n- Email verification\n- 2FA with TOTP\n- 2FA recovery codes\n- Password reset\n- Login throttling and rate limiting\n\n## GitHub repositories\n\n- [Astro](https://github.com/lucia-auth/example-astro-email-password-2fa)\n- [Next.js](https://github.com/lucia-auth/example-nextjs-email-password-2fa)\n- [SvelteKit](https://github.com/lucia-auth/example-sveltekit-email-password-2fa)\n"
  },
  {
    "path": "pages/examples/github-oauth.md",
    "content": "---\ntitle: \"GitHub OAuth\"\n---\n\n# GitHub OAuth\n\nBasic example project with GitHub OAuth and rate limiting.\n\n## GitHub repositories\n\n- [Astro](https://github.com/lucia-auth/example-astro-github-oauth)\n- [Next.js](https://github.com/lucia-auth/example-nextjs-github-oauth)\n- [SvelteKit](https://github.com/lucia-auth/example-sveltekit-github-oauth)\n"
  },
  {
    "path": "pages/examples/google-oauth.md",
    "content": "---\ntitle: \"Google OAuth\"\n---\n\n# Google OAuth\n\nBasic example project with Google OAuth and rate limiting.\n\n## GitHub repositories\n\n- [Astro](https://github.com/lucia-auth/example-astro-google-oauth)\n- [Next.js](https://github.com/lucia-auth/example-nextjs-google-oauth)\n- [SvelteKit](https://github.com/lucia-auth/example-sveltekit-google-oauth)\n"
  },
  {
    "path": "pages/index.md",
    "content": "---\ntitle: \"Lucia\"\n---\n\n# Lucia\n\nLucia is an open source project to provide resources on implementing authentication using JavaScript and TypeScript.\n\nIf you have any questions on auth, feel free to ask them in our [Discord server](https://discord.com/invite/PwrK3kpVR3) or on [GitHub Discussions](https://github.com/lucia-auth/lucia/discussions)!\n\n## Implementation notes\n\n- The code example in this website uses the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) (`crypto`). It's not anything great but it is available in many modern runtimes. Use whatever secure crypto package is available in your runtime.\n- We may also reference packages from the [Oslo project](https://oslojs.dev). As a disclaimer, this package is written by the main author of Lucia. These packages are runtime-agnostic and light-weight, but can be considered as a placeholder for your own implementation or preferred packages.\n- SQLite is used for SQL queries but the TypeScript code uses a placeholder database client.\n\n## Related projects\n\n- [The Copenhagen Book](https://thecopenhagenbook.com): A free online resource covering the various auth concepts in web applications.\n- [Oslo](https://oslojs.dev): Simple, runtime agnostic, and fully-typed packages with minimal dependency for auth and cryptography.\n- [Arctic](https://arcticjs.dev): OAuth 2.0 client library with support for 50+ providers.\n\n## Disclaimer\n\nAll example code in this site is licensed under the [Zero-Clause BSD license](https://github.com/lucia-auth/lucia/blob/main/LICENSE-0BSD). You're free to use, copy, modify, and distribute it without any attribution. The license is approved by the [Open Source Initiative (OSI)](https://opensource.org/license/0bsd) and [Google](https://opensource.google/documentation/reference/patching#forbidden).\n\nEverything else is licensed under the [MIT license](https://github.com/lucia-auth/lucia/blob/main/LICENSE-MIT).\n\n_Copyright © 2024 pilcrow and contributors_\n"
  },
  {
    "path": "pages/lucia-v3/migrate.md",
    "content": "---\ntitle: \"Migrate from Lucia v3\"\n---\n\n# Migrate from Lucia v3\n\nLucia v3 has been deprecated. Lucia is now a learning resource for implementing sessions and more.\n\n## Background\n\nWe ultimately came to the conclusion that it'd be easier and faster to just implement sessions from scratch. The database adapter model wasn't flexible enough for such a low-level library and severely limited the library design.\n\n## Migrating your project\n\nReplacing Lucia v3 with your own implementation should be a straight-forward path, especially since most of your knowledge will still be very useful. No database migrations are necessary.\n\nIf you're fine with invalidating all sessions (and signing out everyone), consider reading through the [new implementation guide](/sessions/basic). The new API is more secure and patches out a very impractical timing attack (see code below for details).\n\n### Sessions\n\n```ts\nfunction generateSessionId(): string {\n\tconst bytes = new Uint8Array(25);\n\tcrypto.getRandomValues(bytes);\n\tconst token = encodeBase32LowerCaseNoPadding(bytes);\n\treturn token;\n}\n\nconst sessionExpiresInSeconds = 60 * 60 * 24 * 30; // 30 days\n\nexport function createSession(dbPool: DBPool, userId: number): Promise<Session> {\n\tconst now = new Date();\n\tconst sessionId = generateSessionId();\n\tconst session: Session = {\n\t\tid: sessionId,\n\t\tuserId,\n\t\texpiresAt: new Date(now.getTime() + 1000 * sessionExpiresInSeconds)\n\t};\n\tawait executeQuery(\n\t\tdbPool,\n\t\t\"INSERT INTO user_session (id, user_id, expires_at) VALUES (?, ?, ?)\",\n\t\t[session.id, session.userId, Math.floor(session.expiresAt.getTime() / 1000)]\n\t);\n\treturn session;\n}\n\nexport function validateSession(dbPool: DBPool, sessionId: string): Promise<Session | null> {\n\tconst now = Date.now();\n\n\t// This may be vulnerable to a timing attack where an attacker can measure the response times\n\t// to guess a valid session ID.\n\t// A more common pattern is a string comparison against a secret using the === operator.\n\t// The === operator is not constant time and the same can be said about SQL = operators.\n\t// Some remote timing attacks has been proven to be possible but there hasn't been a successful\n\t// recorded attack on real-world applications targeting similar vulnerabilities.\n\tconst result = dbPool.executeQuery(\n\t\tdbPool,\n\t\t\"SELECT id, user_id, expires_at FROM session WHERE id = ?\",\n\t\t[sessionId]\n\t);\n\tif (result.rows.length < 1) {\n\t\treturn null;\n\t}\n\tconst row = result.rows[0];\n\tconst session: Session = {\n\t\tid: row[0],\n\t\tuserId: row[1],\n\t\texpiresAt: new Date(row[2] * 1000)\n\t};\n\tif (now.getTime() >= session.expiresAt.getTime()) {\n\t\tawait executeQuery(dbPool, \"DELETE FROM user_session WHERE id = ?\", [session.id]);\n\t\treturn null;\n\t}\n\tif (now.getTime() >= session.expiresAt.getTime() - (1000 * sessionExpiresInSeconds) / 2) {\n\t\tsession.expiresAt = new Date(Date.now() + 1000 * sessionExpiresInSeconds);\n\t\tawait executeQuery(dbPool, \"UPDATE session SET expires_at = ? WHERE id = ?\", [\n\t\t\tMath.floor(session.expiresAt.getTime() / 1000),\n\t\t\tsession.id\n\t\t]);\n\t}\n\treturn session;\n}\n\nexport async function invalidateSession(dbPool: DBPool, sessionId: string): Promise<void> {\n\tawait executeQuery(dbPool, \"DELETE FROM user_session WHERE id = ?\", [sessionId]);\n}\n\nexport async function invalidateAllSessions(dbPool: DBPool, userId: number): Promise<void> {\n\tawait executeQuery(dbPool, \"DELETE FROM user_session WHERE user_id = ?\", [userId]);\n}\n\nexport interface Session {\n\tid: string;\n\tuserId: number;\n\texpiresAt: Date;\n}\n```\n\n### Cookies\n\nCookies should have the following attributes:\n\n- `HttpOnly`: Cookies are only accessible server-side.\n- `SameSite=Lax`: Use Strict for critical websites.\n- `Secure`: Cookies can only be sent over HTTPS (should be omitted when testing on localhost).\n- `Max-Age` or `Expires`: Must be defined to persist cookies.\n- `Path=/`: Cookies can be accessed from all routes.\n\n```ts\nexport function setSessionCookie(response: HTTPResponse, sessionId: string, expiresAt: Date): void {\n\tif (env === ENV.PROD) {\n\t\tresponse.headers.add(\n\t\t\t\"Set-Cookie\",\n\t\t\t`session=${sessionId}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure;`\n\t\t);\n\t} else {\n\t\tresponse.headers.add(\n\t\t\t\"Set-Cookie\",\n\t\t\t`session=${sessionId}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/`\n\t\t);\n\t}\n}\n\n// Set empty session cookie that expires immediately.\nexport function deleteSessionCookie(response: HTTPResponse): void {\n\tif (env === ENV.PROD) {\n\t\tresponse.headers.add(\n\t\t\t\"Set-Cookie\",\n\t\t\t\"session=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure;\"\n\t\t);\n\t} else {\n\t\tresponse.headers.add(\"Set-Cookie\", \"session=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/\");\n\t}\n}\n```\n"
  },
  {
    "path": "pages/rate-limit/token-bucket.md",
    "content": "---\ntitle: \"Token bucket\"\n---\n\n# Token bucket\n\nEach user has their own bucket of tokens that gets refilled at a set interval. A token is removed on every request until none is left and the request is rejected. While a bit more complex than the fixed or sliding window algorithm, it allows you to handle initial bursts and process requests more smoothly overall.\n\n## Memory storage\n\nThis requires the server to persist its memory across requests and will not work in serverless environments.\n\n```ts\nexport class TokenBucketRateLimit<_Key> {\n\tpublic max: number;\n\tpublic refillIntervalSeconds: number;\n\n\tconstructor(max: number, refillIntervalSeconds: number) {\n\t\tthis.max = max;\n\t\tthis.refillIntervalSeconds = refillIntervalSeconds;\n\t}\n\n\tprivate storage = new Map<_Key, Bucket>();\n\n\tpublic consume(key: _Key, cost: number): boolean {\n\t\tlet bucket = this.storage.get(key) ?? null;\n\t\tconst now = Date.now();\n\t\tif (bucket === null) {\n\t\t\tbucket = {\n\t\t\t\tcount: this.max - cost,\n\t\t\t\trefilledAtMilliseconds: now\n\t\t\t};\n\t\t\tthis.storage.set(key, bucket);\n\t\t\treturn true;\n\t\t}\n\t\tconst refill = Math.floor(\n\t\t\t(now - bucket.refilledAtMilliseconds) / (this.refillIntervalSeconds * 1000)\n\t\t);\n\t\tbucket.count = Math.min(bucket.count + refill, this.max);\n\t\tbucket.refilledAtMilliseconds =\n\t\t\tbucket.refilledAtMilliseconds + refill * this.refillIntervalSeconds * 1000;\n\t\tif (bucket.count < cost) {\n\t\t\tthis.storage.set(key, bucket);\n\t\t\treturn false;\n\t\t}\n\t\tbucket.count -= cost;\n\t\tthis.storage.set(key, bucket);\n\t\treturn true;\n\t}\n}\n\ninterface Bucket {\n\tcount: number;\n\trefilledAtMilliseconds: number;\n}\n```\n\n```ts\n// Bucket that has 10 tokens max and refills at a rate of 30 seconds/token\nconst ratelimit = new TokenBucketRateLimit<string>(5, 30);\nconst valid = ratelimit.consume(ip, 1);\nif (!valid) {\n\tthrow new Error(\"Too many requests\");\n}\n```\n\n## Redis\n\nWe'll use Lua scripts to ensure queries are atomic.\n\n```lua\n-- Returns 1 if allowed, 0 if not\nlocal key                   = KEYS[1]\nlocal max                   = tonumber(ARGV[1])\nlocal refillIntervalSeconds = tonumber(ARGV[2])\nlocal cost                  = tonumber(ARGV[3])\nlocal nowMilliseconds       = tonumber(ARGV[4]) -- Current unix time in ms\n\nlocal fields = redis.call(\"HGETALL\", key)\n\nif #fields == 0 then\n\tlocal expiresInSeconds = cost * refillIntervalSeconds\n\tredis.call(\"HSET\", key, \"count\", max - cost, \"refilled_at_ms\", nowMilliseconds)\n\tredis.call(\"EXPIRE\", key, expiresInSeconds)\n\treturn {1}\nend\n\nlocal count = 0\nlocal refilledAtMilliseconds = 0\nfor i = 1, #fields, 2 do\n\tif fields[i] == \"count\" then\n\t\tcount = tonumber(fields[i+1])\n\telseif fields[i] == \"refilled_at_ms\" then\n\t\trefilledAtMilliseconds = tonumber(fields[i+1])\n\tend\nend\n\nlocal refill = math.floor((now - refilledAtMilliseconds) / (refillIntervalSeconds * 1000))\ncount = math.min(count + refill, max)\nrefilledAtMilliseconds = refilledAtMilliseconds + refill * refillIntervalSeconds * 1000\n\nif count < cost then\n\treturn {0}\nend\n\ncount = count - cost\nlocal expiresInSeconds = (max - count) * refillIntervalSeconds\nredis.call(\"HSET\", key, \"count\", count, \"refilled_at_ms\", refilledAtMilliseconds)\nredis.call(\"EXPIRE\", key, expiresInSeconds)\nreturn {1}\n```\n\nLoad the script and retrieve the script hash.\n\n```ts\nconst SCRIPT_SHA = await client.scriptLoad(script);\n```\n\nReference the script with the hash.\n\n```ts\nexport class TokenBucketRateLimit {\n\tprivate storageKey: string;\n\n\tpublic max: number;\n\tpublic refillIntervalSeconds: number;\n\n\tconstructor(storageKey: string, max: number, refillIntervalSeconds: number) {\n\t\tthis.storageKey = storageKey;\n\t\tthis.max = max;\n\t\tthis.refillIntervalSeconds = refillIntervalSeconds;\n\t}\n\n\tpublic async consume(key: string, cost: number): Promise<boolean> {\n\t\tconst key = `token_bucket.v1:${this.storageKey}:${refillIntervalSeconds}:${key}`;\n\t\tconst result = await client.EVALSHA(SCRIPT_SHA, {\n\t\t\tkeys: [key],\n\t\t\targuments: [\n\t\t\t\tthis.max.toString(),\n\t\t\t\tthis.refillIntervalSeconds.toString(),\n\t\t\t\tcost.toString(),\n\t\t\t\tDate.now().toString()\n\t\t\t]\n\t\t});\n\t\treturn Boolean(result[0]);\n\t}\n}\n```\n\n```ts\n// Bucket that has 10 tokens max and refills at a rate of 30 seconds/token\nconst ratelimit = new TokenBucketRateLimit<string>(\"ip\", 5, 30);\nconst valid = await ratelimit.consume(ip, 1);\nif (!valid) {\n\tthrow new Error(\"Too many requests\");\n}\n```\n"
  },
  {
    "path": "pages/sessions/basic.md",
    "content": "---\ntitle: \"Basic session implementation\"\n---\n\n# Basic session implementation\n\n## Overview\n\nSessions have an ID and secret. We're using a separate ID and secret to prevent any possibility of a timing attacks. The secret is hashed before storage to minimize the impact of breaches and leaks.\n\n```ts\ninterface Session {\n\tid: string;\n\tsecretHash: Uint8Array; // Uint8Array is a byte array\n\tcreatedAt: Date;\n}\n```\n\nTokens issued to clients include both the ID and un-hashed secret.\n\n```\n<SESSION_ID>.<SESSION_SECRET>\n```\n\n## Database\n\nThe secret hash is stored as a raw binary value. You can hex- or base64-encode it if you prefer to store it as a string.\n\n```\nCREATE TABLE session (\n\tid TEXT NOT NULL PRIMARY KEY,\n\tsecret_hash BLOB NOT NULL, -- blob is a SQLite data type for raw binary\n\tcreated_at INTEGER NOT NULL -- unix time (seconds)\n) STRICT;\n```\n\n> `STRICT` is an SQLite-specific feature that prevents type coercion.\n\n## Generating IDs and secrets\n\nWe can generate IDs and secrets by generating a random byte array and encoding it into a string.\n\nFor a general purpose ID and secret, we want at least 120 bits of entropy. With 120 bits of entropy, you can generate 1,000,000 IDs/second without worrying about collisions and not ever think about brute force attacks.\n\nSince these strings will be used as secrets as well, it's crucial to use a cryptographically-secure random source. **`Math.random()` should NOT be used for generating secrets.**\n\n```ts\nfunction generateSecureRandomString(): string {\n\t// Human readable alphabet (a-z, 0-9 without l, o, 0, 1 to avoid confusion)\n\tconst alphabet = \"abcdefghijkmnpqrstuvwxyz23456789\";\n\n\t// Generate 24 bytes = 192 bits of entropy.\n\t// We're only going to use 5 bits per byte so the total entropy will be 192 * 5 / 8 = 120 bits\n\tconst bytes = new Uint8Array(24);\n\tcrypto.getRandomValues(bytes);\n\n\tlet id = \"\";\n\tfor (let i = 0; i < bytes.length; i++) {\n\t\t// >> 3 \"removes\" the right-most 3 bits of the byte\n\t\tid += alphabet[bytes[i] >> 3];\n\t}\n\treturn id;\n}\n```\n\n> This encoder wastes 3/8 of the random bits. You can optimize it and get better performance by using all the generated random bits.\n\n## Creating sessions\n\nThe secret is hashed using SHA-256. While SHA-256 is unsuitable for user passwords, because the secret has 120 bits of entropy and already unguessable as is, we can use a fast hashing algorithm here. Even using the fastest or most efficient hardware available, an offline brute-force attack is impossible.\n\n```ts\nasync function createSession(dbPool: DBPool): Promise<SessionWithToken> {\n\tconst now = new Date();\n\n\tconst id = generateSecureRandomString();\n\tconst secret = generateSecureRandomString();\n\tconst secretHash = await hashSecret(secret);\n\n\tconst token = id + \".\" + secret;\n\n\tconst session: SessionWithToken = {\n\t\tid,\n\t\tsecretHash,\n\t\tcreatedAt: now,\n\t\ttoken\n\t};\n\n\tawait executeQuery(dbPool, \"INSERT INTO session (id, secret_hash, created_at) VALUES (?, ?, ?)\", [\n\t\tsession.id,\n\t\tsession.secretHash,\n\t\tMath.floor(session.createdAt.getTime() / 1000)\n\t]);\n\n\treturn session;\n}\n\nasync function hashSecret(secret: string): Promise<Uint8Array> {\n\tconst secretBytes = new TextEncoder().encode(secret);\n\tconst secretHashBuffer = await crypto.subtle.digest(\"SHA-256\", secretBytes);\n\treturn new Uint8Array(secretHashBuffer);\n}\n\ninterface SessionWithToken extends Session {\n\ttoken: string;\n}\n\ninterface Session {\n\t// ...\n}\n```\n\n## Validating session tokens\n\nTo validate a sessions token, parse out the ID and secret, get the session with the ID, check the expiration, and compare the secret against the hash. Use constant-time comparison for checking secrets and derived hashes.\n\nWe recommend setting an expiration for all sessions. Implement an [inactivity timeout](/sessions/inactivity-timeout) instead if you want to keep active users signed in.\n\n```ts\nconst sessionExpiresInSeconds = 60 * 60 * 24; // 1 day\n\nasync function createSession(dbPool: DBPool): Promise<SessionWithToken> {\n\t// ...\n}\n\nasync function validateSessionToken(dbPool: DBPool, token: string): Promise<Session | null> {\n\tconst tokenParts = token.split(\".\");\n\tif (tokenParts.length !== 2) {\n\t\treturn null;\n\t}\n\tconst sessionId = tokenParts[0];\n\tconst sessionSecret = tokenParts[1];\n\n\tconst session = await getSession(dbPool, sessionId);\n\tif (!session) {\n\t\treturn null;\n\t}\n\n\tconst tokenSecretHash = await hashSecret(sessionSecret);\n\tconst validSecret = constantTimeEqual(tokenSecretHash, session.secretHash);\n\tif (!validSecret) {\n\t\treturn null;\n\t}\n\n\treturn session;\n}\n\nasync function getSession(dbPool: DBPool, sessionId: string): Promise<Session | null> {\n\tconst now = new Date();\n\n\tconst result = await executeQuery(\n\t\tdbPool,\n\t\t\"SELECT id, secret_hash, created_at FROM session WHERE id = ?\",\n\t\t[sessionId]\n\t);\n\tif (result.rows.length !== 1) {\n\t\treturn null;\n\t}\n\tconst row = result.rows[0];\n\tconst session: Session = {\n\t\tid: row[0],\n\t\tsecretHash: row[1],\n\t\tcreatedAt: new Date(row[2] * 1000)\n\t};\n\n\t// Check expiration\n\tif (now.getTime() - session.createdAt.getTime() >= sessionExpiresInSeconds * 1000) {\n\t\tawait deleteSession(sessionId);\n\t\treturn null;\n\t}\n\n\treturn session;\n}\n\nasync function deleteSession(dbPool: DBPool, sessionId: string): Promise<void> {\n\tawait executeQuery(dbPool, \"DELETE FROM session WHERE id = ?\", [sessionId]);\n}\n\nasync function hashSecret(secret: string): Promise<Uint8Array> {\n\t// ...\n}\n\ninterface SessionWithToken extends Session {\n\t// ...\n}\n\ninterface Session {\n\t// ...\n}\n```\n\n```ts\nfunction constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {\n\tif (a.byteLength !== b.byteLength) {\n\t\treturn false;\n\t}\n\tlet c = 0;\n\tfor (let i = 0; i < a.byteLength; i++) {\n\t\tc |= a[i] ^ b[i];\n\t}\n\treturn c === 0;\n}\n```\n\n## Client-side storage\n\nFor most websites, we recommend storing the session in a `HttpOnly`, `Secure`, cookie with `SameSite` set to `Lax`. It's important to note that using a `HttpOnly` cookie does not make you immune to targeted XSS attacks.\n\nCookies usually have a maximum lifetime of 400 days. If you want a persistent session, set a new cookie periodically.\n\n```\nSet-Cookie: session_token=SESSION_TOKEN; Max-Age=86400; HttpOnly; Secure; Path=/; SameSite=Lax\n```\n\nIf you have a separate auth server that cannot be hosted on the same domain, you can store the session as a cookie or in `localStorage`. Storing credentials in storage accessible by client-side JavaScript, you may be more vulnerable to supply-chain attacks. Because some browsers and extensions can clear non-`HttpOnly` cookies, we recommended storing the token in a `HttpOnly` cookie as well so you have the option to store the cookie client-side again.\n\nFor native applications, use the device's built-in secure storage.\n\n## Securing secret hashes\n\nIf you have endpoints that can return a session object, ensure that the session secret hash is omitted. Instead of using `JSON.stringify()` directly, we recommend creating a dedicated function for encoding the session object into JSON.\n\n```ts\nfunction encodeSessionPublicJSON(session: Session): string {\n\t// Omit Session.secretHash\n\tconst json = JSON.stringify({\n\t\tid: session.id,\n\t\tcreated_at: Math.floor(session.createdAt.getTime() / 1000)\n\t});\n\treturn json;\n}\n```\n\n## CSRF protection\n\nCross-site request forgery protection must be implemented for websites that uses cookies and accepts form submissions. Even if you only have endpoints that accept JSON request bodies, implementing a basic protection is recommended.\n\nWhile the `SameSite` cookie attribute provides some CSRF protection, it doesn't protect your website from subdomain takeovers and the cookie will be set without the attribute on older browsers.\n\nFor websites only targeting modern browsers (post-2020), the `Origin` header can be used to check the request origin. Requests without the `Origin` header should be blocked with a status of `403` or similar. Some frameworks already have a similar CSRF protection built in, including Next.js (only for server actions), SvelteKit, and Astro (v5+).\n\n```ts\nfunction verifyRequestOrigin(method: string, originHeader: string): boolean {\n\tif (method === \"GET\" || method === \"HEAD\") {\n\t\treturn true;\n\t}\n\treturn originHeader === \"example.com\";\n}\n\n// Enable strict origin check only on production environments.\nfunction verifyRequestOrigin(method: string, originHeader: string): boolean {\n\tif (env !== ENV.PROD) {\n\t\treturn true;\n\t}\n\tif (method === \"GET\" || method === \"HEAD\") {\n\t\treturn true;\n\t}\n\treturn originHeader === \"example.com\";\n}\n```\n\nTo support older browsers, use an anti-CSRF token stored in the server or the [signed double-submit cookies](https://thecopenhagenbook.com/csrf#signed-double-submit-cookies) for a stateless approach.\n"
  },
  {
    "path": "pages/sessions/frameworks/index.md",
    "content": "---\ntitle: \"Framework-specific implementation notes\"\n---\n\n# Framework-specific implementation notes\n\nThese pages cover some implementation notes for specific frameworks.\n\n- [Next.js](/sessions/frameworks/nextjs)\n- [Sveltekit](/sessions/frameworks/sveltekit)\n"
  },
  {
    "path": "pages/sessions/frameworks/nextjs.md",
    "content": "---\ntitle: \"Next.js implementation notes\"\n---\n\n# Next.js implementation notes\n\n## Validating sessions\n\nWe recommend creating a reusable `getCurrentSession()` function that wraps the validation logic with `cache()` so it can be called multiple times without causing multiple database calls.\n\n```ts\nimport { cookies } from \"next/headers\";\nimport { cache } from \"react\";\n\nexport const getCurrentSession = cache(async (): Promise<Session> => {\n\tconst cookieStore = await cookies();\n\tconst token = cookieStore.get(\"session\")?.value ?? null;\n\tif (token === null) {\n\t\treturn { session: null, user: null };\n\t}\n\tconst result = await validateSessionToken(token);\n\treturn result;\n});\n```\n\n## Persisting cookies\n\nYou cannot set cookies with `cookies()` or `headers()` when rendering routes. This will throw an error:\n\n```tsx\nimport { cookies } from \"next/headers\";\n\nexport default function Page() {\n\tconst cookieStore = await cookies();\n\tcookieStore.set(\"message\", \"hello\");\n\t// ...\n}\n```\n\nThis becomes an issue if you want to persist session cookies by continuously setting a new cookie. We recommend using Next.js middleware for this instead.\n\n```ts\nexport function middleware(request: NextRequest) {\n\tconst response = NextResponse.next();\n\n\tconst sessionToken = request.cookies.get(\"session\")?.value ?? null;\n\n\tif (sessionToken !== null) {\n\t\t// Re-set the cookie with updated expiration\n\t\tresponse.cookies.set({\n\t\t\tname: \"session\",\n\t\t\tvalue: sessionToken,\n\t\t\tmaxAge: 60 * 60 * 24 * 365, // 1 year\n\t\t\tpath: \"/\",\n\t\t\thttpOnly: true,\n\t\t\tsecure: process.env.NODE_ENV === \"production\"\n\t\t});\n\t}\n\n\treturn response;\n}\n```\n"
  },
  {
    "path": "pages/sessions/frameworks/sveltekit.md",
    "content": "---\ntitle: \"SvelteKit implementation notes\"\n---\n\n# SvelteKit implementation notes\n\n## Authorization check with layouts\n\nA server load function inside `+layout.server.ts` will not run on navigation between pages nested inside it. For example, a load function in `+layout.server.ts` will not run when navigating between `/` and `/foo`. This means that anyone can skip layout server load functions.\n\n```\nroutes/\n    +layout.server.ts\n    +page.svelte\n    foo/\n        +page.svelte\n```\n\nAs such, sessions must be validated on a per-request basis by putting authorization checks in each `+page.server.ts` load function or in a `handle()` hook.\n"
  },
  {
    "path": "pages/sessions/inactivity-timeout.md",
    "content": "---\ntitle: \"Inactivity timeout\"\n---\n\n# Inactivity timeout\n\nThis page builds upon the [Basic session implementation](/sessions/basic) page.\n\nSetting an expiration for sessions is recommended, but it'd be annoying if active users were constantly signed-out. Instead of just removing the expiration all together, we recommend implementing an inactivity timeout as a replacement for it. This ensures active users remain signed in while inactive users are signed out after a set period.\n\nFirst add a `lastVerifiedAt` attribute to your sessions. This would include when the session token was last verified.\n\n```ts\ninterface Session {\n\tid: string;\n\tsecretHash: Uint8Array;\n\tlastVerifiedAt: Date;\n\tcreatedAt: Date;\n}\n```\n\n```\nCREATE TABLE session (\n\tid TEXT NOT NULL PRIMARY KEY,\n\tsecret_hash BLOB NOT NULL,\n\tlast_verified_at INTEGER NOT NULL, -- unix (seconds)\n\tcreated_at INTEGER NOT NULL,\n) STRICT;\n```\n\nWhile we can update the attribute after every verification, that would increase our database load dramatically. Instead, we can update the `lastVerifiedAt` attribute after a set period, e.g. 1 hour. It is important to only update the attribute _after_ the token has been verified.\n\nFinally, invalidate sessions that haven't been used recently. Anywhere from 1 day to 30 days would work depending on your application and type of session.\n\n```ts\nconst inactivityTimeoutSeconds = 60 * 60 * 24 * 10; // 10 days\nconst activityCheckIntervalSeconds = 60 * 60; // 1 hour\n\nasync function validateSessionToken(dbPool: DBPool, token: string): Promise<Session | null> {\n\tconst now = new Date();\n\n\tconst tokenParts = token.split(\".\");\n\tif (tokenParts.length !== 2) {\n\t\treturn null;\n\t}\n\tconst sessionId = tokenParts[0];\n\tconst sessionSecret = tokenParts[1];\n\n\tconst session = await getSession(dbPool, sessionId);\n\tif (!session) {\n\t\treturn null;\n\t}\n\n\tconst tokenSecretHash = await hashSecret(sessionSecret);\n\tconst validSecret = constantTimeEqual(tokenSecretHash, session.secretHash);\n\tif (!validSecret) {\n\t\treturn null;\n\t}\n\n\tif (now.getTime() - session.lastVerifiedAt.getTime() >= activityCheckIntervalSeconds * 1000) {\n\t\tsession.lastVerifiedAt = now;\n\t\tawait executeQuery(dbPool, \"UPDATE session SET last_verified_at = ? WHERE id = ?\", [\n\t\t\tMath.floor(session.lastVerifiedAt.getTime() / 1000),\n\t\t\tsessionId\n\t\t]);\n\t}\n\n\treturn session;\n}\n\nasync function getSession(dbPool: DBPool, sessionId: string): Promise<Session | null> {\n\tconst now = new Date();\n\n\tconst result = await executeQuery(\n\t\tdbPool,\n\t\t\"SELECT id, secret_hash, last_verified_at, created_at FROM session WHERE id = ?\",\n\t\t[sessionId]\n\t);\n\tif (result.rows.length !== 1) {\n\t\treturn null;\n\t}\n\tconst row = result.rows[0];\n\tconst session: Session = {\n\t\tid: row[0],\n\t\tsecretHash: row[1],\n\t\tlastVerifiedAt: new Date(row[2] * 1000),\n\t\tcreatedAt: new Date(row[3] * 1000)\n\t};\n\n\t// Inactivity timeout\n\tif (now.getTime() - session.lastVerifiedAt.getTime() >= inactivityTimeoutSeconds * 1000) {\n\t\tawait deleteSession(dbPool, sessionId);\n\t\treturn null;\n\t}\n\n\treturn session;\n}\n```\n"
  },
  {
    "path": "pages/sessions/overview.md",
    "content": "---\ntitle: \"Sessions\"\n---\n\n# Sessions\n\nHTTP is by design a stateless protocol. The server doesn't know if 2 requests came from the same client.\n\nBrowsers offer client-side storage cookies and local storage but you can't trust anything sent by the client. If you identify users with a \"user\" cookie, how do you stop users from editing the value and impersonating other users? How do you keep all that state in the server?\n\nThis is where sessions come in. Whenever you want to start persisting state across requests, for example a \"signed in\" state, you create a session. Requests associated with a session share the same state, for example the current authenticated users. To allow clients to associate a request with a session, you can issue session tokens. Assuming that token is unguessable, you can assume requests with the token are linked to that particular session.\n\nLearn how to implement a basic session securely by reading the [Basic session implementation](/sessions/basic) page. We also recommend looking at the [Inactivity timeout](/sessions/inactivity-timeout) page if you plan to use sessions for user authentication.\n"
  },
  {
    "path": "pages/sessions/stateless-tokens.md",
    "content": "---\ntitle: \"Stateless tokens\"\n---\n\n# Stateless tokens\n\nThis page builds upon the [Basic session implementation](/sessions/basic) page.\n\nStateless tokens are self-validating tokens, with the most common format being JSON Web Tokens (JWTs). Using them as session tokens can reduce how often your database needs to be queried when validating sessions. They token body may look like something like this:\n\n```json\n{\n\t\"session\": {\n\t\t\"id\": \"SESSION_ID\",\n\t\t\"created_at\": 946684800 // unix (seconds)\n\t},\n\t\"iat\": 946684800,\n\t\"exp\": 946684860\n}\n```\n\nHowever, because they're stateless they can't be invalidated since there's no \"valid/invalid\" state tracked on the server. They only become invalid when they expire. As such, we recommend using a short-lived JWT alongside regular session tokens.\n\n```ts\nconst session = await createSession();\nconst sessionJWT = await createSessionJWT(session);\n```\n\nTo validate sessions, first validate the JWT and validate the session token if the JWT is invalid.\n\n```ts\nlet sessionToken: string;\nlet sessionJWT: string;\n\nlet validatedSession = await validateSessionJWT(sessionJWT);\n// If jwt is invalid/expired, check the main session token.\nif (validatedSession === null) {\n\tvalidatedSession = await validateSessionToken(sessionToken);\n}\nif (validatedSession === null) {\n\t// no session\n}\n```\n\nHere is a basic JWT implementation with HMAC SHA-256. The JWT should only be valid for at most 5 minutes. We recommend using an asymmetric signing algorithm like Ed25519 or ECDSA if sessions are issued by a main auth server but needs to be validated in many servers. Make sure to securely store and manage your signing key.\n\n> [`jose`](https://github.com/panva/jose) and [`jsonwebtoken`](https://github.com/auth0/node-jsonwebtoken) are popular NPM packages for creating and validating JWTs.\n\n```ts\nimport * as oslo_encoding from \"@oslojs/encoding\";\n\n// Randomly generated key\n// For HMAC with SHA-256, the key must be 32 bytes\nconst jwtHS256Key = new Uint8Array(32);\n\nasync function createSessionJWT(session: Session): Promise<string> {\n\tconst now = new Date();\n\n\tconst expirationSeconds = 60; // 1 minute\n\n\tconst headerJSON = JSON.stringify({ alg: \"HS256\", typ: \"JWT\" });\n\tconst headerJSONBytes = new TextEncoder().encode(headerJSON);\n\tconst encodedHeader = oslo_encoding.encodeBase64url(headerJSONBytes);\n\n\tconst bodyJSON = JSON.stringify({\n\t\t// Omit the secret hash\n\t\tsession: {\n\t\t\tid: session.id,\n\t\t\tcreated_at: Math.floor(session.createdAt.getTime() / 1000)\n\t\t},\n\t\tiat: Math.floor(now.getTime() / 1000),\n\t\texp: Math.floor(now.getTime() / 1000) + expirationSeconds\n\t});\n\tconst bodyJSONBytes = new TextEncoder().encode(bodyJSON);\n\tconst encodedBody = oslo_encoding.encodeBase64url(bodyJSONBytes);\n\n\tconst headerAndBody = encodedHeader + \".\" + encodedBody;\n\tconst headerAndBodyBytes = new TextEncoder().encode(headerAndBody);\n\n\tconst hmacCryptoKey = await crypto.subtle.importKey(\n\t\t\"raw\",\n\t\tjwtHS256Key,\n\t\t{\n\t\t\tname: \"HMAC\",\n\t\t\thash: \"SHA-256\"\n\t\t},\n\t\tfalse\n\t);\n\tconst signature = await crypto.subtle.sign(\"HMAC\", hmacCryptoKey, headerAndBodyBytes);\n\tconst encodedSignature = oslo_jwt.encodeJWT(headerJSON, bodyJSON);\n\n\tconst jw = headerAndBody + \".\" + encodedSignature;\n\treturn jwt;\n}\n\nasync function validateSessionJWT(jwt: string): Promise<ValidatedSession | null> {\n\tconst now = new Date();\n\n\tconst parts = jwt.split(\".\");\n\tif (parts.length !== 3) {\n\t\treturn null;\n\t}\n\n\t// Parse header\n\tlet header: object;\n\ttry {\n\t\tconst headerJSONBytes = oslo_encoding.decodeBase64url(parts[0]);\n\t\tconst headerJSON = new TextDecoder().decode(headerJSONBytes);\n\t\tconst parsedHeader = JSON.parse(headerJSON) as unknown;\n\t\tif (typeof parsedHeader !== \"object\" || parsedHeader === null) {\n\t\t\treturn null;\n\t\t}\n\t\theader = parsedHeader;\n\t} catch {\n\t\treturn null;\n\t}\n\n\t// Verify header claims\n\tif (\"typ\" in header && header.typ !== \"JWT\") {\n\t\treturn null;\n\t}\n\tif (!(\"alg\" in header) || header.alg !== \"HS256\") {\n\t\treturn null;\n\t}\n\n\t// Verify signature\n\tconst signature = oslo_encoding.decodeBase64url(parts[2]);\n\tconst headerAndBodyBytes = new TextEncoder().encode(parts[0] + \".\" + parts[1]);\n\tconst hmacCryptoKey = await crypto.subtle.importKey(\n\t\t\"raw\",\n\t\tjwtHS256Key,\n\t\t{\n\t\t\tname: \"HMAC\",\n\t\t\thash: \"SHA-256\"\n\t\t},\n\t\tfalse\n\t);\n\tconst validSignature = await crypto.subtle.verify(\n\t\t\"HMAC\",\n\t\thmacCryptoKey,\n\t\tsignature,\n\t\theaderAndBodyBytes\n\t);\n\tif (!validSignature) {\n\t\treturn null;\n\t}\n\n\t// Parse body\n\tlet body: object;\n\ttry {\n\t\tconst bodyJSONParts = oslo_encoding.decodeBase64url(parts[1]);\n\t\tconst bodyJSON = new TextDecoder().decode(bodyJSONParts);\n\t\tconst parsedBody = JSON.parse(bodyJSON) as unknown;\n\t\tif (typeof parsedBody !== \"object\" || parsedBody === null) {\n\t\t\treturn null;\n\t\t}\n\t\tbody = parsedBody;\n\t} catch {\n\t\treturn null;\n\t}\n\n\t// Check expiration\n\tif (!(\"exp\" in body) || typeof body.exp !== \"number\") {\n\t\treturn null;\n\t}\n\tconst expiresAt = new Date(body.exp * 1000);\n\tif (now.getTime() >= expiresAt.getTime()) {\n\t\treturn null;\n\t}\n\n\t// Parse session\n\tif (!(\"session\" in body) || typeof body.session !== \"object\" || body.session === null) {\n\t\treturn null;\n\t}\n\tconst parsedSession = body.session;\n\tif (!(\"id\" in parsedSession) || typeof parsedSession.id !== \"string\") {\n\t\treturn null;\n\t}\n\tif (!(\"created_at\" in parsedSession) || typeof parsedSession.created_at !== \"number\") {\n\t\treturn null;\n\t}\n\n\tconst session: ValidatedSession = {\n\t\tid: parsedSession.id,\n\t\tcreatedAt: new Date(parsedSession.created_at * 1000)\n\t};\n\treturn session;\n}\n\ninterface ValidatedSession {\n\tid: string;\n\tcreatedAt: Date;\n}\n```\n"
  },
  {
    "path": "pages/tutorials/github-oauth/astro.md",
    "content": "---\ntitle: \"Tutorial: GitHub OAuth in Astro\"\n---\n\n# Tutorial: GitHub OAuth in Astro\n\n_Before starting, make sure you've created the session and cookie API outlined in the [Sessions](/sessions/overview) page._\n\nAn [example project](https://github.com/lucia-auth/example-astro-github-oauth) based on this tutorial is also available. You can clone the example locally or [open it in StackBlitz](https://stackblitz.com/github/lucia-auth/example-astro-github-oauth).\n\n```\ngit clone git@github.com:lucia-auth/example-astro-github-oauth.git\n```\n\n## Create an OAuth App\n\n[Create a GitHub OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app). Set the redirect URI to `http://localhost:4321/login/github/callback`. Copy and paste the client ID and secret to your `.env` file.\n\n```bash\n# .env\nGITHUB_CLIENT_ID=\"\"\nGITHUB_CLIENT_SECRET=\"\"\n```\n\n## Update database\n\nUpdate your user model to include the user's GitHub ID and username.\n\n```ts\ninterface User {\n\tid: number;\n\tgithubId: number;\n\tusername: string;\n}\n```\n\n## Setup Arctic\n\nWe recommend using [Arctic](https://arcticjs.dev) for implementing OAuth. Arctic is a lightweight OAuth client library that supports 50+ providers out of the box.\n\n```\nnpm install arctic\n```\n\nInitialize the GitHub provider with the client ID and secret.\n\n```ts\nimport { GitHub } from \"arctic\";\n\nexport const github = new GitHub(\n\timport.meta.env.GITHUB_CLIENT_ID,\n\timport.meta.env.GITHUB_CLIENT_SECRET,\n\tnull\n);\n```\n\n## Sign in page\n\nCreate `pages/login/index.astro` and add a basic sign in button, which should be a link to `/login/github`.\n\n```html\n<!-- pages/login/index.astro -->\n<html lang=\"en\">\n\t<body>\n\t\t<h1>Sign in</h1>\n\t\t<a href=\"/login/github\">Sign in with GitHub</a>\n\t</body>\n</html>\n```\n\n## Create authorization URL\n\nCreate an API route in `pages/login/github/index.ts`. Generate a new state and create a new authorization URL. Store the state and redirect the user to the authorization URL. The user will be redirected to GitHub's sign in page.\n\n```ts\n// pages/login/github/index.ts\nimport { generateState } from \"arctic\";\nimport { github } from \"@lib/oauth\";\n\nimport type { APIContext } from \"astro\";\n\nexport async function GET(context: APIContext): Promise<Response> {\n\tconst state = generateState();\n\tconst url = github.createAuthorizationURL(state, []);\n\n\tcontext.cookies.set(\"github_oauth_state\", state, {\n\t\tpath: \"/\",\n\t\tsecure: import.meta.env.PROD,\n\t\thttpOnly: true,\n\t\tmaxAge: 60 * 10, // 10 minutes\n\t\tsameSite: \"lax\"\n\t});\n\n\treturn context.redirect(url.toString());\n}\n```\n\n## Validate callback\n\nCreate an API route in `pages/login/github/callback.ts` to handle the callback. Check that the state in the URL matches the one that's stored. Then, validate the authorization code and stored code verifier. Use the access token to get the user's profile with the GitHub API. Check if the user is already registered; if not, create a new user. Finally, create a new session and set the session cookie to complete the authentication process.\n\n```ts\n// pages/login/github/callback.ts\nimport { generateSessionToken, createSession, setSessionTokenCookie } from \"@lib/session\";\nimport { github } from \"@lib/oauth\";\n\nimport type { APIContext } from \"astro\";\nimport type { OAuth2Tokens } from \"arctic\";\n\nexport async function GET(context: APIContext): Promise<Response> {\n\tconst code = context.url.searchParams.get(\"code\");\n\tconst state = context.url.searchParams.get(\"state\");\n\tconst storedState = context.cookies.get(\"github_oauth_state\")?.value ?? null;\n\tif (code === null || state === null || storedState === null) {\n\t\treturn new Response(null, {\n\t\t\tstatus: 400\n\t\t});\n\t}\n\tif (state !== storedState) {\n\t\treturn new Response(null, {\n\t\t\tstatus: 400\n\t\t});\n\t}\n\n\tlet tokens: OAuth2Tokens;\n\ttry {\n\t\ttokens = await github.validateAuthorizationCode(code);\n\t} catch (e) {\n\t\t// Invalid code or client credentials\n\t\treturn new Response(null, {\n\t\t\tstatus: 400\n\t\t});\n\t}\n\tconst githubUserResponse = await fetch(\"https://api.github.com/user\", {\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${tokens.accessToken()}`\n\t\t}\n\t});\n\tconst githubUser = await githubUserResponse.json();\n\tconst githubUserId = githubUser.id;\n\tconst githubUsername = githubUser.login;\n\n\t// TODO: Replace this with your own DB query.\n\tconst existingUser = await getUserFromGitHubId(githubUserId);\n\n\tif (existingUser !== null) {\n\t\tconst sessionToken = generateSessionToken();\n\t\tconst session = await createSession(sessionToken, existingUser.id);\n\t\tsetSessionTokenCookie(context, sessionToken, session.expiresAt);\n\t\treturn context.redirect(\"/\");\n\t}\n\n\t// TODO: Replace this with your own DB query.\n\tconst user = await createUser(githubUserId, githubUsername);\n\n\tconst sessionToken = generateSessionToken();\n\tconst session = await createSession(sessionToken, user.id);\n\tsetSessionTokenCookie(context, sessionToken, session.expiresAt);\n\treturn context.redirect(\"/\");\n}\n```\n\n## Get the current user\n\nIf you implemented the middleware outlined in the [Session cookies in Astro](/sessions/cookies/astro) page, you can get the current session and user from `Locals`.\n\n```ts\nif (Astro.locals.user === null) {\n\treturn Astro.redirect(\"/login\");\n}\n\nconst user = Astro.locals.user;\n```\n\n## Sign out\n\nSign out users by invalidating their session. Make sure to remove the session cookie as well.\n\n```ts\nimport { invalidateSession, deleteSessionTokenCookie } from \"@lib/session\";\n\nimport type { APIContext } from \"astro\";\n\nexport async function POST(context: APIContext): Promise<Response> {\n\tif (context.locals.session === null) {\n\t\treturn new Response(null, {\n\t\t\tstatus: 401\n\t\t});\n\t}\n\tawait invalidateSession(context.locals.session.id);\n\tdeleteSessionTokenCookie(context);\n\treturn context.redirect(\"/login\");\n}\n```\n"
  },
  {
    "path": "pages/tutorials/github-oauth/index.md",
    "content": "---\ntitle: \"Tutorial: GitHub OAuth\"\n---\n\n# Tutorial: GitHub OAuth\n\nIn this tutorial, you'll learn how to authenticate users with GitHub and persist sessions with the API you created.\n\n- [Astro](/tutorials/github-oauth/astro)\n- [Next.js](/tutorials/github-oauth/nextjs)\n- [SvelteKit](/tutorials/github-oauth/sveltekit)\n"
  },
  {
    "path": "pages/tutorials/github-oauth/nextjs.md",
    "content": "---\ntitle: \"Tutorial: GitHub OAuth in Next.js\"\n---\n\n# Tutorial: GitHub OAuth in Next.js\n\n_Before starting, make sure you've created the session and cookie API outlined in the [Sessions](/sessions/overview) page._\n\nAn [example project](https://github.com/lucia-auth/example-nextjs-github-oauth) based on this tutorial is also available. You can clone the example locally or [open it in StackBlitz](https://stackblitz.com/github/lucia-auth/example-nextjs-github-oauth).\n\n```\ngit clone git@github.com:lucia-auth/example-nextjs-github-oauth.git\n```\n\n## Create an OAuth App\n\n[Create a GitHub OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app). Set the redirect URI to `http://localhost:3000/login/github/callback`. Copy and paste the client ID and secret to your `.env` file.\n\n```bash\n# .env\nGITHUB_CLIENT_ID=\"\"\nGITHUB_CLIENT_SECRET=\"\"\n```\n\n## Update database\n\nUpdate your user model to include the user's GitHub ID and username.\n\n```ts\ninterface User {\n\tid: number;\n\tgithubId: number;\n\tusername: string;\n}\n```\n\n## Setup Arctic\n\nWe recommend using [Arctic](https://arcticjs.dev) for implementing OAuth. Arctic is a lightweight OAuth client library that supports 50+ providers out of the box.\n\n```\nnpm install arctic\n```\n\nInitialize the GitHub provider with the client ID and secret.\n\n```ts\nimport { GitHub } from \"arctic\";\n\nexport const github = new GitHub(\n\tprocess.env.GITHUB_CLIENT_ID,\n\tprocess.env.GITHUB_CLIENT_SECRET,\n\tnull\n);\n```\n\n## Sign in page\n\nCreate `app/login/page.tsx` and add a basic sign in button, which should be a link to `/login/github`.\n\n```tsx\n// app/login/page.tsx\nexport default async function Page() {\n\treturn (\n\t\t<>\n\t\t\t<h1>Sign in</h1>\n\t\t\t<a href=\"/login/github\">Sign in with GitHub</a>\n\t\t</>\n\t);\n}\n```\n\n## Create authorization URL\n\nCreate an Route Handlers in `app/login/github/route.ts`. Generate a new state and create a new authorization URL. Store the state and redirect the user to the authorization URL. The user will be redirected to GitHub's sign in page.\n\n```ts\n// app/login/github/route.ts\nimport { generateState } from \"arctic\";\nimport { github } from \"@/lib/oauth\";\nimport { cookies } from \"next/headers\";\n\nexport async function GET(): Promise<Response> {\n\tconst state = generateState();\n\tconst url = github.createAuthorizationURL(state, []);\n\n\tconst cookieStore = await cookies();\n\tcookieStore.set(\"github_oauth_state\", state, {\n\t\tpath: \"/\",\n\t\tsecure: process.env.NODE_ENV === \"production\",\n\t\thttpOnly: true,\n\t\tmaxAge: 60 * 10,\n\t\tsameSite: \"lax\"\n\t});\n\n\treturn new Response(null, {\n\t\tstatus: 302,\n\t\theaders: {\n\t\t\tLocation: url.toString()\n\t\t}\n\t});\n}\n```\n\n## Validate callback\n\nCreate an Route Handlers in `app/login/github/callback/route.ts` to handle the callback. Check that the state in the URL matches the one that's stored. Then, validate the authorization code and stored code verifier. Use the access token to get the user's profile with the GitHub API. Check if the user is already registered; if not, create a new user. Finally, create a new session and set the session cookie to complete the authentication process.\n\n```ts\n// app/login/github/callback/route.ts\nimport { generateSessionToken, createSession, setSessionTokenCookie } from \"@/lib/session\";\nimport { github } from \"@/lib/oauth\";\nimport { cookies } from \"next/headers\";\n\nimport type { OAuth2Tokens } from \"arctic\";\n\nexport async function GET(request: Request): Promise<Response> {\n\tconst url = new URL(request.url);\n\tconst code = url.searchParams.get(\"code\");\n\tconst state = url.searchParams.get(\"state\");\n\tconst cookieStore = await cookies();\n\tconst storedState = cookieStore.get(\"github_oauth_state\")?.value ?? null;\n\tif (code === null || state === null || storedState === null) {\n\t\treturn new Response(null, {\n\t\t\tstatus: 400\n\t\t});\n\t}\n\tif (state !== storedState) {\n\t\treturn new Response(null, {\n\t\t\tstatus: 400\n\t\t});\n\t}\n\n\tlet tokens: OAuth2Tokens;\n\ttry {\n\t\ttokens = await github.validateAuthorizationCode(code);\n\t} catch (e) {\n\t\t// Invalid code or client credentials\n\t\treturn new Response(null, {\n\t\t\tstatus: 400\n\t\t});\n\t}\n\tconst githubUserResponse = await fetch(\"https://api.github.com/user\", {\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${tokens.accessToken()}`\n\t\t}\n\t});\n\tconst githubUser = await githubUserResponse.json();\n\tconst githubUserId = githubUser.id;\n\tconst githubUsername = githubUser.login;\n\n\t// TODO: Replace this with your own DB query.\n\tconst existingUser = await getUserFromGitHubId(githubUserId);\n\n\tif (existingUser !== null) {\n\t\tconst sessionToken = generateSessionToken();\n\t\tconst session = await createSession(sessionToken, existingUser.id);\n\t\tawait setSessionTokenCookie(sessionToken, session.expiresAt);\n\t\treturn new Response(null, {\n\t\t\tstatus: 302,\n\t\t\theaders: {\n\t\t\t\tLocation: \"/\"\n\t\t\t}\n\t\t});\n\t}\n\n\t// TODO: Replace this with your own DB query.\n\tconst user = await createUser(githubUserId, githubUsername);\n\n\tconst sessionToken = generateSessionToken();\n\tconst session = await createSession(sessionToken, user.id);\n\tawait setSessionTokenCookie(sessionToken, session.expiresAt);\n\treturn new Response(null, {\n\t\tstatus: 302,\n\t\theaders: {\n\t\t\tLocation: \"/\"\n\t\t}\n\t});\n}\n```\n\n## Validate requests\n\nUse the `getCurrentSession()` function from the [Session cookies in Next.js](/sessions/cookies/nextjs) page to get the current user and session.\n\n```tsx\nimport { redirect } from \"next/navigation\";\nimport { getCurrentSession } from \"@/lib/session\";\n\nexport default async function Page() {\n\tconst { user } = await getCurrentSession();\n\tif (user === null) {\n\t\treturn redirect(\"/login\");\n\t}\n\treturn <h1>Hi, {user.username}!</h1>;\n}\n```\n\n## Sign out\n\nSign out users by invalidating their session. Make sure to remove the session cookie as well.\n\n```tsx\nimport { getCurrentSession, invalidateSession, deleteSessionTokenCookie } from \"@/lib/session\";\nimport { redirect } from \"next/navigation\";\nimport { cookies } from \"next/headers\";\n\nexport default async function Page() {\n\treturn (\n\t\t<form action={logout}>\n\t\t\t<button>Sign out</button>\n\t\t</form>\n\t);\n}\n\nasync function logout(): Promise<ActionResult> {\n\t\"use server\";\n\tconst { session } = await getCurrentSession();\n\tif (!session) {\n\t\treturn {\n\t\t\terror: \"Unauthorized\"\n\t\t};\n\t}\n\n\tawait invalidateSession(session.id);\n\tawait deleteSessionTokenCookie();\n\treturn redirect(\"/login\");\n}\n\ninterface ActionResult {\n\terror: string | null;\n}\n```\n"
  },
  {
    "path": "pages/tutorials/github-oauth/sveltekit.md",
    "content": "---\ntitle: \"Tutorial: GitHub OAuth in SvelteKit\"\n---\n\n# Tutorial: GitHub OAuth in SvelteKit\n\n_Before starting, make sure you've created the session and cookie API outlined in the [Sessions](/sessions/overview) page._\n\nAn [example project](https://github.com/lucia-auth/example-sveltekit-github-oauth) based on this tutorial is also available. You can clone the example locally or [open it in StackBlitz](https://stackblitz.com/github/lucia-auth/example-sveltekit-github-oauth).\n\n```\ngit clone git@github.com:lucia-auth/example-sveltekit-github-oauth.git\n```\n\n## Create an OAuth App\n\n[Create a GitHub OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app). Set the redirect URI to `http://localhost:5173/login/github/callback`. Copy and paste the client ID and secret to your `.env` file.\n\n```bash\n# .env\nGITHUB_CLIENT_ID=\"\"\nGITHUB_CLIENT_SECRET=\"\"\n```\n\n## Update database\n\nUpdate your user model to include the user's GitHub ID and username.\n\n```ts\ninterface User {\n\tid: number;\n\tgithubId: number;\n\tusername: string;\n}\n```\n\n## Setup Arctic\n\nWe recommend using [Arctic](https://arcticjs.dev) for implementing OAuth. Arctic is a lightweight OAuth client library that supports 50+ providers out of the box.\n\n```\nnpm install arctic\n```\n\nInitialize the GitHub provider with the client ID and secret.\n\n```ts\nimport { GitHub } from \"arctic\";\nimport { GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET } from \"$env/static/private\";\n\nexport const github = new GitHub(GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, null);\n```\n\n## Sign in page\n\nCreate `routes/login/+page.svelte` and add a basic sign in button, which should be a link to `/login/github`.\n\n```svelte\n<!-- routes/login/+page.svelte -->\n<h1>Sign in</h1>\n<a href=\"/login/github\">Sign in with GitHub</a>\n```\n\n## Create authorization URL\n\nCreate an API route in `routes/login/github/+server.ts`. Generate a new state and create a new authorization URL. Store the state and redirect the user to the authorization URL. The user will be redirected to GitHub's sign in page.\n\n```ts\n// routes/login/github/+server.ts\nimport { generateState } from \"arctic\";\nimport { github } from \"$lib/server/oauth\";\n\nimport type { RequestEvent } from \"@sveltejs/kit\";\n\nexport async function GET(event: RequestEvent): Promise<Response> {\n\tconst state = generateState();\n\tconst url = github.createAuthorizationURL(state, []);\n\n\tevent.cookies.set(\"github_oauth_state\", state, {\n\t\tpath: \"/\",\n\t\thttpOnly: true,\n\t\tmaxAge: 60 * 10,\n\t\tsameSite: \"lax\"\n\t});\n\n\treturn new Response(null, {\n\t\tstatus: 302,\n\t\theaders: {\n\t\t\tLocation: url.toString()\n\t\t}\n\t});\n}\n```\n\n## Validate callback\n\nCreate an API route in `routes/login/github/callback/+server.ts` to handle the callback. Check that the state in the URL matches the one that's stored. Then, validate the authorization code and stored code verifier. Use the access token to get the user's profile with the GitHub API. Check if the user is already registered; if not, create a new user. Finally, create a new session and set the session cookie to complete the authentication process.\n\n```ts\n// routes/login/github/callback/+server.ts\nimport { generateSessionToken, createSession, setSessionTokenCookie } from \"$lib/server/session\";\nimport { github } from \"$lib/server/oauth\";\n\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport type { OAuth2Tokens } from \"arctic\";\n\nexport async function GET(event: RequestEvent): Promise<Response> {\n\tconst code = event.url.searchParams.get(\"code\");\n\tconst state = event.url.searchParams.get(\"state\");\n\tconst storedState = event.cookies.get(\"github_oauth_state\") ?? null;\n\tif (code === null || state === null || storedState === null) {\n\t\treturn new Response(null, {\n\t\t\tstatus: 400\n\t\t});\n\t}\n\tif (state !== storedState) {\n\t\treturn new Response(null, {\n\t\t\tstatus: 400\n\t\t});\n\t}\n\n\tlet tokens: OAuth2Tokens;\n\ttry {\n\t\ttokens = await github.validateAuthorizationCode(code);\n\t} catch (e) {\n\t\t// Invalid code or client credentials\n\t\treturn new Response(null, {\n\t\t\tstatus: 400\n\t\t});\n\t}\n\tconst githubUserResponse = await fetch(\"https://api.github.com/user\", {\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${tokens.accessToken()}`\n\t\t}\n\t});\n\tconst githubUser = await githubUserResponse.json();\n\tconst githubUserId = githubUser.id;\n\tconst githubUsername = githubUser.login;\n\n\t// TODO: Replace this with your own DB query.\n\tconst existingUser = await getUserFromGitHubId(githubUserId);\n\n\tif (existingUser) {\n\t\tconst sessionToken = generateSessionToken();\n\t\tconst session = await createSession(sessionToken, existingUser.id);\n\t\tsetSessionTokenCookie(event, sessionToken, session.expiresAt);\n\t\treturn new Response(null, {\n\t\t\tstatus: 302,\n\t\t\theaders: {\n\t\t\t\tLocation: \"/\"\n\t\t\t}\n\t\t});\n\t}\n\n\t// TODO: Replace this with your own DB query.\n\tconst user = await createUser(githubUserId, githubUsername);\n\n\tconst sessionToken = generateSessionToken();\n\tconst session = await createSession(sessionToken, user.id);\n\tsetSessionTokenCookie(event, sessionToken, session.expiresAt);\n\n\treturn new Response(null, {\n\t\tstatus: 302,\n\t\theaders: {\n\t\t\tLocation: \"/\"\n\t\t}\n\t});\n}\n```\n\n## Get the current user\n\nIf you implemented the middleware outlined in the [Session cookies in SvelteKit](/sessions/cookies/sveltekit) page, you can get the current session and user from `Locals`.\n\n```ts\n// routes/+page.server.ts\nimport { redirect } from \"@sveltejs/kit\";\n\nimport type { PageServerLoad } from \"./$types\";\n\nexport const load: PageServerLoad = async (event) => {\n\tif (!event.locals.user) {\n\t\treturn redirect(302, \"/login\");\n\t}\n\n\treturn {\n\t\tuser\n\t};\n};\n```\n\n## Sign out\n\nSign out users by invalidating their session. Make sure to remove the session cookie as well.\n\n```ts\n// routes/+page.server.ts\nimport { fail, redirect } from \"@sveltejs/kit\";\nimport { invalidateSession, deleteSessionTokenCookie } from \"$lib/server/session\";\n\nimport type { Actions, PageServerLoad } from \"./$types\";\n\nexport const load: PageServerLoad = async ({ locals }) => {\n\t// ...\n};\n\nexport const actions: Actions = {\n\tdefault: async (event) => {\n\t\tif (event.locals.session === null) {\n\t\t\treturn fail(401);\n\t\t}\n\t\tawait invalidateSession(event.locals.session.id);\n\t\tdeleteSessionTokenCookie(event);\n\t\treturn redirect(302, \"/login\");\n\t}\n};\n```\n\n```svelte\n<!-- routes/+page.svelte -->\n<script lang=\"ts\">\n\timport { enhance } from \"$app/forms\";\n</script>\n\n<form method=\"post\" use:enhance>\n    <button>Sign out</button>\n</form>\n```\n"
  },
  {
    "path": "pages/tutorials/google-oauth/astro.md",
    "content": "---\ntitle: \"Tutorial: Google OAuth in Astro\"\n---\n\n# Tutorial: Google OAuth in Astro\n\n_Before starting, make sure you've created the session and cookie API outlined in the [Sessions](/sessions/overview) page._\n\nAn [example project](https://github.com/lucia-auth/example-astro-google-oauth) based on this tutorial is also available. You can clone the example locally or [open it in StackBlitz](https://stackblitz.com/github/lucia-auth/example-astro-google-oauth).\n\n```\ngit clone git@github.com:lucia-auth/example-astro-google-oauth.git\n```\n\n## Create an OAuth App\n\nCreate an Google OAuth client on the Cloud Console. Set the redirect URI to `http://localhost:4321/login/google/callback`. Copy and paste the client ID and secret to your `.env` file.\n\n```bash\n# .env\nGOOGLE_CLIENT_ID=\"\"\nGOOGLE_CLIENT_SECRET=\"\"\n```\n\n## Update database\n\nUpdate your user model to include the user's Google ID and username.\n\n```ts\ninterface User {\n\tid: number;\n\tgoogleId: string;\n\tname: string;\n}\n```\n\n## Setup Arctic\n\nWe recommend using [Arctic](https://arcticjs.dev) for implementing OAuth. Arctic is a lightweight OAuth client library that supports 50+ providers out of the box.\n\n```\nnpm install arctic\n```\n\nInitialize the Google provider with the client ID and secret.\n\n```ts\nimport { Google } from \"arctic\";\n\nexport const google = new Google(\n\timport.meta.env.GOOGLE_CLIENT_ID,\n\timport.meta.env.GOOGLE_CLIENT_SECRET,\n\t\"http://localhost:4321/login/google/callback\"\n);\n```\n\n## Sign in page\n\nCreate `pages/login/index.astro` and add a basic sign in button, which should be a link to `/login/google`.\n\n```html\n<!-- pages/login/index.astro -->\n<html lang=\"en\">\n\t<body>\n\t\t<h1>Sign in</h1>\n\t\t<a href=\"/login/google\">Sign in with Google</a>\n\t</body>\n</html>\n```\n\n## Create authorization URL\n\nCreate an API route in `pages/login/google/index.ts`. Generate a new state and code verifier, and create a new authorization URL. Add the `openid` and `profile` scope to have access to the user's profile later on. Store the state and code verifier, and redirect the user to the authorization URL. The user will be redirected to Google's sign in page.\n\n```ts\n// pages/login/google/index.ts\nimport { generateState } from \"arctic\";\nimport { google } from \"@lib/oauth\";\n\nimport type { APIContext } from \"astro\";\n\nexport async function GET(context: APIContext): Promise<Response> {\n\tconst state = generateState();\n\tconst codeVerifier = generateCodeVerifier();\n\tconst url = google.createAuthorizationURL(state, codeVerifier, [\"openid\", \"profile\"]);\n\n\tcontext.cookies.set(\"google_oauth_state\", state, {\n\t\tpath: \"/\",\n\t\tsecure: import.meta.env.PROD,\n\t\thttpOnly: true,\n\t\tmaxAge: 60 * 10, // 10 minutes\n\t\tsameSite: \"lax\"\n\t});\n\tcontext.cookies.set(\"google_code_verifier\", codeVerifier, {\n\t\tpath: \"/\",\n\t\tsecure: import.meta.env.PROD,\n\t\thttpOnly: true,\n\t\tmaxAge: 60 * 10, // 10 minutes\n\t\tsameSite: \"lax\"\n\t});\n\n\treturn context.redirect(url.toString());\n}\n```\n\n## Validate callback\n\nCreate an API route in `pages/login/google/callback.ts` to handle the callback. Check that the state in the URL matches the one that's stored. Then, validate the authorization code and stored code verifier. If you passed the `openid` and `profile` scope, Google will return a token ID with the user's profile. Check if the user is already registered; if not, create a new user. Finally, create a new session and set the session cookie to complete the authentication process.\n\n```ts\n// pages/login/google/callback.ts\nimport { generateSessionToken, createSession, setSessionTokenCookie } from \"@lib/server/session\";\nimport { google } from \"@lib/oauth\";\nimport { decodeIdToken } from \"arctic\";\n\nimport type { APIContext } from \"astro\";\nimport type { OAuth2Tokens } from \"arctic\";\n\nexport async function GET(context: APIContext): Promise<Response> {\n\tconst code = context.url.searchParams.get(\"code\");\n\tconst state = context.url.searchParams.get(\"state\");\n\tconst storedState = context.cookies.get(\"google_oauth_state\")?.value ?? null;\n\tconst codeVerifier = context.cookies.get(\"google_code_verifier\")?.value ?? null;\n\tif (code === null || state === null || storedState === null || codeVerifier === null) {\n\t\treturn new Response(null, {\n\t\t\tstatus: 400\n\t\t});\n\t}\n\tif (state !== storedState) {\n\t\treturn new Response(null, {\n\t\t\tstatus: 400\n\t\t});\n\t}\n\n\tlet tokens: OAuth2Tokens;\n\ttry {\n\t\ttokens = await google.validateAuthorizationCode(code, codeVerifier);\n\t} catch (e) {\n\t\t// Invalid code or client credentials\n\t\treturn new Response(null, {\n\t\t\tstatus: 400\n\t\t});\n\t}\n\tconst claims = decodeIdToken(tokens.idToken());\n\tconst googleUserId = claims.sub;\n\tconst username = claims.name;\n\n\t// TODO: Replace this with your own DB query.\n\tconst existingUser = await getUserFromGoogleId(googleUserId);\n\n\tif (existingUser !== null) {\n\t\tconst sessionToken = generateSessionToken();\n\t\tconst session = await createSession(sessionToken, existingUser.id);\n\t\tsetSessionTokenCookie(context, sessionToken, session.expiresAt);\n\t\treturn context.redirect(\"/\");\n\t}\n\n\t// TODO: Replace this with your own DB query.\n\tconst user = await createUser(googleUserId, username);\n\n\tconst sessionToken = generateSessionToken();\n\tconst session = await createSession(sessionToken, user.id);\n\tsetSessionTokenCookie(context, sessionToken, session.expiresAt);\n\treturn context.redirect(\"/\");\n}\n```\n\n## Get the current user\n\nIf you implemented the middleware outlined in the [Session cookies in Astro](/sessions/cookies/astro) page, you can get the current session and user from `Locals`.\n\n```ts\nif (Astro.locals.user === null) {\n\treturn Astro.redirect(\"/login\");\n}\n\nconst user = Astro.locals.user;\n```\n\n## Sign out\n\nSign out users by invalidating their session. Make sure to remove the session cookie as well.\n\n```ts\nimport { invalidateSession, deleteSessionTokenCookie } from \"@lib/server/session\";\n\nimport type { APIContext } from \"astro\";\n\nexport async function POST(context: APIContext): Promise<Response> {\n\tif (context.locals.session === null) {\n\t\treturn new Response(null, {\n\t\t\tstatus: 401\n\t\t});\n\t}\n\tawait invalidateSession(context.locals.session.id);\n\tdeleteSessionTokenCookie(context);\n\treturn context.redirect(\"/login\");\n}\n```\n"
  },
  {
    "path": "pages/tutorials/google-oauth/index.md",
    "content": "---\ntitle: \"Tutorial: Google OAuth\"\n---\n\n# Tutorial: Google OAuth\n\nIn this tutorial, you'll learn how to authenticate users with Google and persist sessions with the API you created.\n\n- [Astro](/tutorials/google-oauth/astro)\n- [Next.js](/tutorials/google-oauth/nextjs)\n- [SvelteKit](/tutorials/google-oauth/sveltekit)\n"
  },
  {
    "path": "pages/tutorials/google-oauth/nextjs.md",
    "content": "---\ntitle: \"Tutorial: Google OAuth in Next.js\"\n---\n\n# Tutorial: Google OAuth in Next.js\n\n_Before starting, make sure you've created the session and cookie API outlined in the [Sessions](/sessions/overview) page._\n\nAn [example project](https://github.com/lucia-auth/example-nextjs-google-oauth) based on this tutorial is also available. You can clone the example locally or [open it in StackBlitz](https://stackblitz.com/github/lucia-auth/example-nextjs-google-oauth).\n\n```\ngit clone git@github.com:lucia-auth/example-nextjs-google-oauth.git\n```\n\n## Create an OAuth App\n\nCreate an Google OAuth client on the Cloud Console. Set the redirect URI to `http://localhost:3000/login/google/callback`. Copy and paste the client ID and secret to your `.env` file.\n\n```bash\n# .env\nGOOGLE_CLIENT_ID=\"\"\nGOOGLE_CLIENT_SECRET=\"\"\n```\n\n## Update database\n\nUpdate your user model to include the user's Google ID and username.\n\n```ts\ninterface User {\n\tid: number;\n\tgoogleId: string;\n\tname: string;\n}\n```\n\n## Setup Arctic\n\n```\nnpm install arctic\n```\n\nInitialize the Google provider with the client ID, client secret, and redirect URI.\n\n```ts\nimport { Google } from \"arctic\";\n\nexport const google = new Google(\n\tprocess.env.GOOGLE_CLIENT_ID,\n\tprocess.env.GOOGLE_CLIENT_SECRET,\n\t\"http://localhost:3000/login/google/callback\"\n);\n```\n\n## Sign in page\n\nCreate `app/login/page.tsx` and add a basic sign in button, which should be a link to `/login/google`.\n\n```tsx\n// app/login/page.tsx\nexport default async function Page() {\n\treturn (\n\t\t<>\n\t\t\t<h1>Sign in</h1>\n\t\t\t<a href=\"/login/google\">Sign in with Google</a>\n\t\t</>\n\t);\n}\n```\n\n## Create authorization URL\n\nCreate an API route in `app/login/google/route.ts`. Generate a new state and code verifier, and create a new authorization URL. Add the `openid` and `profile` scope to have access to the user's profile later on. Store the state and code verifier, and redirect the user to the authorization URL. The user will be redirected to Google's sign in page.\n\n```ts\n// app/login/google/route.ts\nimport { generateState, generateCodeVerifier } from \"arctic\";\nimport { google } from \"@/lib/auth\";\nimport { cookies } from \"next/headers\";\n\nexport async function GET(): Promise<Response> {\n\tconst state = generateState();\n\tconst codeVerifier = generateCodeVerifier();\n\tconst url = google.createAuthorizationURL(state, codeVerifier, [\"openid\", \"profile\"]);\n\n\tconst cookieStore = await cookies();\n\tcookieStore.set(\"google_oauth_state\", state, {\n\t\tpath: \"/\",\n\t\thttpOnly: true,\n\t\tsecure: process.env.NODE_ENV === \"production\",\n\t\tmaxAge: 60 * 10, // 10 minutes\n\t\tsameSite: \"lax\"\n\t});\n\tcookieStore.set(\"google_code_verifier\", codeVerifier, {\n\t\tpath: \"/\",\n\t\thttpOnly: true,\n\t\tsecure: process.env.NODE_ENV === \"production\",\n\t\tmaxAge: 60 * 10, // 10 minutes\n\t\tsameSite: \"lax\"\n\t});\n\n\treturn new Response(null, {\n\t\tstatus: 302,\n\t\theaders: {\n\t\t\tLocation: url.toString()\n\t\t}\n\t});\n}\n```\n\n## Validate callback\n\nCreate an Route Handlers in `app/login/google/callback/route.ts` to handle the callback. Check that the state in the URL matches the one that's stored. Then, validate the authorization code and stored code verifier. If you passed the `openid` and `profile` scope, Google will return a token ID with the user's profile. Check if the user is already registered; if not, create a new user. Finally, create a new session and set the session cookie to complete the authentication process.\n\n```ts\n// app/login/google/callback/route.ts\nimport { generateSessionToken, createSession, setSessionTokenCookie } from \"@/lib/session\";\nimport { google } from \"@/lib/oauth\";\nimport { cookies } from \"next/headers\";\nimport { decodeIdToken } from \"arctic\";\n\nimport type { OAuth2Tokens } from \"arctic\";\n\nexport async function GET(request: Request): Promise<Response> {\n\tconst url = new URL(request.url);\n\tconst code = url.searchParams.get(\"code\");\n\tconst state = url.searchParams.get(\"state\");\n\tconst cookieStore = await cookies();\n\tconst storedState = cookieStore.get(\"google_oauth_state\")?.value ?? null;\n\tconst codeVerifier = cookieStore.get(\"google_code_verifier\")?.value ?? null;\n\tif (code === null || state === null || storedState === null || codeVerifier === null) {\n\t\treturn new Response(null, {\n\t\t\tstatus: 400\n\t\t});\n\t}\n\tif (state !== storedState) {\n\t\treturn new Response(null, {\n\t\t\tstatus: 400\n\t\t});\n\t}\n\n\tlet tokens: OAuth2Tokens;\n\ttry {\n\t\ttokens = await google.validateAuthorizationCode(code, codeVerifier);\n\t} catch (e) {\n\t\t// Invalid code or client credentials\n\t\treturn new Response(null, {\n\t\t\tstatus: 400\n\t\t});\n\t}\n\tconst claims = decodeIdToken(tokens.idToken());\n\tconst googleUserId = claims.sub;\n\tconst username = claims.name;\n\n\t// TODO: Replace this with your own DB query.\n\tconst existingUser = await getUserFromGoogleId(googleUserId);\n\n\tif (existingUser !== null) {\n\t\tconst sessionToken = generateSessionToken();\n\t\tconst session = await createSession(sessionToken, existingUser.id);\n\t\tawait setSessionTokenCookie(sessionToken, session.expiresAt);\n\t\treturn new Response(null, {\n\t\t\tstatus: 302,\n\t\t\theaders: {\n\t\t\t\tLocation: \"/\"\n\t\t\t}\n\t\t});\n\t}\n\n\t// TODO: Replace this with your own DB query.\n\tconst user = await createUser(googleUserId, username);\n\n\tconst sessionToken = generateSessionToken();\n\tconst session = await createSession(sessionToken, user.id);\n\tawait setSessionTokenCookie(sessionToken, session.expiresAt);\n\treturn new Response(null, {\n\t\tstatus: 302,\n\t\theaders: {\n\t\t\tLocation: \"/\"\n\t\t}\n\t});\n}\n```\n\n## Validate requests\n\nUse the `getCurrentSession()` function from the [Session cookies in Next.js](/sessions/cookies/nextjs) page to get the current user and session.\n\n```tsx\nimport { redirect } from \"next/navigation\";\nimport { getCurrentSession } from \"@/lib/session\";\n\nexport default async function Page() {\n\tconst { user } = await getCurrentSession();\n\tif (user === null) {\n\t\treturn redirect(\"/login\");\n\t}\n\treturn <h1>Hi, {user.name}!</h1>;\n}\n```\n\n## Sign out\n\nSign out users by invalidating their session. Make sure to remove the session cookie as well.\n\n```tsx\nimport { getCurrentSession, invalidateSession, deleteSessionTokenCookie } from \"@/lib/session\";\nimport { redirect } from \"next/navigation\";\nimport { cookies } from \"next/headers\";\n\nexport default async function Page() {\n\treturn (\n\t\t<form action={logout}>\n\t\t\t<button>Sign out</button>\n\t\t</form>\n\t);\n}\n\nasync function logout(): Promise<ActionResult> {\n\t\"use server\";\n\tconst { session } = await getCurrentSession();\n\tif (!session) {\n\t\treturn {\n\t\t\terror: \"Unauthorized\"\n\t\t};\n\t}\n\n\tawait invalidateSession(session.id);\n\tawait deleteSessionTokenCookie();\n\treturn redirect(\"/login\");\n}\n\ninterface ActionResult {\n\terror: string | null;\n}\n```\n"
  },
  {
    "path": "pages/tutorials/google-oauth/sveltekit.md",
    "content": "---\ntitle: \"Tutorial: Google OAuth in SvelteKit\"\n---\n\n# Tutorial: Google OAuth in SvelteKit\n\n_Before starting, make sure you've created the session and cookie API outlined in the [Sessions](/sessions/overview) page._\n\nAn [example project](https://github.com/lucia-auth/example-sveltekit-google-oauth) based on this tutorial is also available. You can clone the example locally or [open it in StackBlitz](https://stackblitz.com/github/lucia-auth/example-sveltekit-google-oauth).\n\n```\ngit clone git@github.com:lucia-auth/example-sveltekit-google-oauth.git\n```\n\n## Create an OAuth App\n\nCreate an Google OAuth client on the Cloud Console. Set the redirect URI to `http://localhost:5173/login/google/callback`. Copy and paste the client ID and secret to your `.env` file.\n\n```bash\n# .env\nGOOGLE_CLIENT_ID=\"\"\nGOOGLE_CLIENT_SECRET=\"\"\n```\n\n## Update database\n\nUpdate your user model to include the user's Google ID and username.\n\n```ts\ninterface User {\n\tid: number;\n\tgoogleId: string;\n\tname: string;\n}\n```\n\n## Setup Arctic\n\n```\nnpm install arctic\n```\n\nInitialize the Google provider with the client ID, client secret, and redirect URI.\n\n```ts\nimport { Google } from \"arctic\";\nimport { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET } from \"$env/static/private\";\n\nexport const google = new Google(\n\tGOOGLE_CLIENT_ID,\n\tGOOGLE_CLIENT_SECRET,\n\t\"http://localhost:5173/login/google/callback\"\n);\n```\n\n## Sign in page\n\nCreate `routes/login/+page.svelte` and add a basic sign in button, which should be a link to `/login/google`.\n\n```svelte\n<!-- routes/login/+page.svelte -->\n<h1>Sign in</h1>\n<a href=\"/login/google\">Sign in with Google</a>\n```\n\n## Create authorization URL\n\nCreate an API route in `routes/login/google/+server.ts`. Generate a new state and code verifier, and create a new authorization URL. Add the `openid` and `profile` scope to have access to the user's profile later on. Store the state and code verifier, and redirect the user to the authorization URL. The user will be redirected to Google's sign in page.\n\n```ts\n// routes/login/google/+server.ts\nimport { generateState, generateCodeVerifier } from \"arctic\";\nimport { google } from \"$lib/server/oauth\";\n\nimport type { RequestEvent } from \"@sveltejs/kit\";\n\nexport async function GET(event: RequestEvent): Promise<Response> {\n\tconst state = generateState();\n\tconst codeVerifier = generateCodeVerifier();\n\tconst url = google.createAuthorizationURL(state, codeVerifier, [\"openid\", \"profile\"]);\n\n\tevent.cookies.set(\"google_oauth_state\", state, {\n\t\tpath: \"/\",\n\t\thttpOnly: true,\n\t\tmaxAge: 60 * 10, // 10 minutes\n\t\tsameSite: \"lax\"\n\t});\n\tevent.cookies.set(\"google_code_verifier\", codeVerifier, {\n\t\tpath: \"/\",\n\t\thttpOnly: true,\n\t\tmaxAge: 60 * 10, // 10 minutes\n\t\tsameSite: \"lax\"\n\t});\n\n\treturn new Response(null, {\n\t\tstatus: 302,\n\t\theaders: {\n\t\t\tLocation: url.toString()\n\t\t}\n\t});\n}\n```\n\n## Validate callback\n\nCreate an API route in `routes/login/google/callback/+server.ts` to handle the callback. Check that the state in the URL matches the one that's stored. Then, validate the authorization code and stored code verifier. If you passed the `openid` and `profile` scope, Google will return a token ID with the user's profile. Check if the user is already registered; if not, create a new user. Finally, create a new session and set the session cookie to complete the authentication process.\n\n```ts\n// routes/login/google/callback/+server.ts\nimport { generateSessionToken, createSession, setSessionTokenCookie } from \"$lib/server/session\";\nimport { google } from \"$lib/server/oauth\";\nimport { decodeIdToken } from \"arctic\";\n\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport type { OAuth2Tokens } from \"arctic\";\n\nexport async function GET(event: RequestEvent): Promise<Response> {\n\tconst code = event.url.searchParams.get(\"code\");\n\tconst state = event.url.searchParams.get(\"state\");\n\tconst storedState = event.cookies.get(\"google_oauth_state\") ?? null;\n\tconst codeVerifier = event.cookies.get(\"google_code_verifier\") ?? null;\n\tif (code === null || state === null || storedState === null || codeVerifier === null) {\n\t\treturn new Response(null, {\n\t\t\tstatus: 400\n\t\t});\n\t}\n\tif (state !== storedState) {\n\t\treturn new Response(null, {\n\t\t\tstatus: 400\n\t\t});\n\t}\n\n\tlet tokens: OAuth2Tokens;\n\ttry {\n\t\ttokens = await google.validateAuthorizationCode(code, codeVerifier);\n\t} catch (e) {\n\t\t// Invalid code or client credentials\n\t\treturn new Response(null, {\n\t\t\tstatus: 400\n\t\t});\n\t}\n\tconst claims = decodeIdToken(tokens.idToken());\n\tconst googleUserId = claims.sub;\n\tconst username = claims.name;\n\n\t// TODO: Replace this with your own DB query.\n\tconst existingUser = await getUserFromGoogleId(googleUserId);\n\n\tif (existingUser !== null) {\n\t\tconst sessionToken = generateSessionToken();\n\t\tconst session = await createSession(sessionToken, existingUser.id);\n\t\tsetSessionTokenCookie(event, sessionToken, session.expiresAt);\n\t\treturn new Response(null, {\n\t\t\tstatus: 302,\n\t\t\theaders: {\n\t\t\t\tLocation: \"/\"\n\t\t\t}\n\t\t});\n\t}\n\n\t// TODO: Replace this with your own DB query.\n\tconst user = await createUser(googleUserId, username);\n\n\tconst sessionToken = generateSessionToken();\n\tconst session = await createSession(sessionToken, user.id);\n\tsetSessionTokenCookie(event, sessionToken, session.expiresAt);\n\treturn new Response(null, {\n\t\tstatus: 302,\n\t\theaders: {\n\t\t\tLocation: \"/\"\n\t\t}\n\t});\n}\n```\n\n## Get the current user\n\nIf you implemented the middleware outlined in the [Session cookies in SvelteKit](/sessions/cookies/sveltekit) page, you can get the current session and user from `Locals`.\n\n```ts\n// routes/+page.server.ts\nimport { redirect } from \"@sveltejs/kit\";\n\nimport type { PageServerLoad } from \"./$types\";\n\nexport const load: PageServerLoad = async (event) => {\n\tif (!event.locals.user) {\n\t\treturn redirect(302, \"/login\");\n\t}\n\n\treturn {\n\t\tuser\n\t};\n};\n```\n\n## Sign out\n\nSign out users by invalidating their session. Make sure to remove the session cookie as well.\n\n```ts\n// routes/+page.server.ts\nimport { fail, redirect } from \"@sveltejs/kit\";\nimport { invalidateSession, deleteSessionTokenCookie } from \"$lib/server/session\";\n\nimport type { Actions, PageServerLoad } from \"./$types\";\n\nexport const load: PageServerLoad = async ({ locals }) => {\n\t// ...\n};\n\nexport const actions: Actions = {\n\tdefault: async (event) => {\n\t\tif (event.locals.session === null) {\n\t\t\treturn fail(401);\n\t\t}\n\t\tawait invalidateSession(event.locals.session.id);\n\t\tdeleteSessionTokenCookie(event);\n\t\treturn redirect(302, \"/login\");\n\t}\n};\n```\n\n```svelte\n<!-- routes/+page.svelte -->\n<script lang=\"ts\">\n\timport { enhance } from \"$app/forms\";\n</script>\n\n<form method=\"post\" use:enhance>\n    <button>Sign out</button>\n</form>\n```\n"
  }
]