Repository: lucia-auth/lucia
Branch: main
Commit: bcee386807d6
Files: 32
Total size: 77.4 KB
Directory structure:
gitextract_16u519e7/
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ └── publish.yaml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── LICENSE-0BSD
├── LICENSE-MIT
├── README.md
├── malta.config.json
├── package.json
└── pages/
├── examples/
│ ├── email-password-2fa-webauthn.md
│ ├── email-password-2fa.md
│ ├── github-oauth.md
│ └── google-oauth.md
├── index.md
├── lucia-v3/
│ └── migrate.md
├── rate-limit/
│ └── token-bucket.md
├── sessions/
│ ├── basic.md
│ ├── frameworks/
│ │ ├── index.md
│ │ ├── nextjs.md
│ │ └── sveltekit.md
│ ├── inactivity-timeout.md
│ ├── overview.md
│ └── stateless-tokens.md
└── tutorials/
├── github-oauth/
│ ├── astro.md
│ ├── index.md
│ ├── nextjs.md
│ └── sveltekit.md
└── google-oauth/
├── astro.md
├── index.md
├── nextjs.md
└── sveltekit.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
github: pilcrowOnPaper
================================================
FILE: .github/workflows/publish.yaml
================================================
name: "Publish"
on:
push:
branches:
- main
env:
CLOUDFLARE_API_TOKEN: ${{secrets.CLOUDFLARE_PAGES_API_TOKEN}}
jobs:
publish:
name: Publish
runs-on: ubuntu-latest
steps:
- name: setup actions
uses: actions/checkout@v3
- name: setup node
uses: actions/setup-node@v3
with:
node-version: 20.5.1
registry-url: https://registry.npmjs.org
- name: install malta
run: |
curl -o malta.tgz -L https://github.com/pilcrowonpaper/malta/releases/latest/download/linux-amd64.tgz
tar -xvzf malta.tgz
- name: build
run: ./linux-amd64/malta build
- name: install wrangler
run: npm i -g wrangler
- name: deploy
run: wrangler pages deploy dist --project-name lucia --branch main
================================================
FILE: .gitignore
================================================
dist
pnpm-lock.yaml
node_modules
package-lock.json
.DS_Store
================================================
FILE: .prettierignore
================================================
.DS_Store
node_modules
/dist
pnpm-lock.yaml
package-lock.json
yarn.lock
================================================
FILE: .prettierrc.json
================================================
{
"useTabs": true,
"trailingComma": "none",
"printWidth": 100
}
================================================
FILE: LICENSE-0BSD
================================================
Copyright (c) 2024 pilcrowOnPaper and contributors
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
================================================
FILE: LICENSE-MIT
================================================
Copyright (c) 2024 pilcrowOnPaper and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Lucia
**Link: [lucia-auth.com](https://lucia-auth.com)**
> [!IMPORTANT]
> 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.
Lucia is an open source project to provide resources on implementing authentication with JavaScript and TypeScript.
The 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.
If 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)!
## Why not a library?
We've found it extremely hard to develop a library that:
1. Supports the many database libraries, ORMs, frameworks, runtimes, and deployment options available in the ecosystem.
2. Provides enough flexibility for the majority of use cases.
3. Does not add significant complexity to projects.
We 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.
## Related projects
- [The Copenhagen Book](https://thecopenhagenbook.com): A free online resource covering the various auth concepts in web applications.
- [Oslo](https://oslojs.dev): Simple, runtime agnostic, and fully-typed packages with minimal dependency for auth and cryptography.
- [Arctic](https://arcticjs.dev): OAuth 2.0 client library with support for 50+ providers.
## Disclaimer
All 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).
Everything else this repository is licensed under the [MIT license](https://github.com/lucia-auth/lucia/blob/main/LICENSE-MIT).
_Copyright © 2024 pilcrow and contributors_
================================================
FILE: malta.config.json
================================================
{
"name": "Lucia",
"description": "An open source resource on implementing authentication with JavaScript",
"domain": "https://lucia-auth.com",
"twitter": "@lucia_auth",
"asset_hashing": true,
"sidebar": [
{
"title": "Sessions",
"pages": [
["Overview", "/sessions/overview"],
["Basic implementation", "/sessions/basic"],
["Inactivity timeout", "/sessions/inactivity-timeout"],
["Stateless tokens", "/sessions/stateless-tokens"],
["Frameworks", "/sessions/frameworks"]
]
},
{
"title": "Tutorials",
"pages": [
["GitHub OAuth", "/tutorials/github-oauth"],
["Google OAuth", "/tutorials/google-oauth"]
]
},
{
"title": "Example projects",
"pages": [
["GitHub OAuth", "/examples/github-oauth"],
["Google OAuth", "/examples/google-oauth"],
["Email and password with 2FA", "/examples/email-password-2fa"],
["Email and password with 2FA and WebAuthn", "/examples/email-password-2fa-webauthn"]
]
},
{
"title": "Rate limiting",
"pages": [["Token bucket", "/rate-limit/token-bucket"]]
},
{
"title": "Lucia v3",
"pages": [["Migrate", "/lucia-v3/migrate"]]
},
{
"title": "Community",
"pages": [
["GitHub", "https://github.com/lucia-auth/lucia"],
["Discord", "https://discord.com/invite/PwrK3kpVR3"],
["Twitter", "https://x.com/lucia_auth"],
["Donate", "https://github.com/sponsors/pilcrowOnPaper"]
]
},
{
"title": "Related projects",
"pages": [
["The Copenhagen Book", "https://thecopenhagenbook.com"],
["Oslo", "https://oslojs.dev"],
["Arctic", "https://arcticjs.dev"]
]
}
]
}
================================================
FILE: package.json
================================================
{
"name": "lucia",
"scripts": {
"format": "prettier -w ."
},
"repository": {
"type": "git",
"url": "https://github.com/lucia-auth/lucia"
},
"author": "pilcrowOnPaper",
"license": "MIT",
"devDependencies": {
"prettier": "^3.0.3"
}
}
================================================
FILE: pages/examples/email-password-2fa-webauthn.md
================================================
---
title: "Email and password with 2FA and WebAuthn"
---
# Email and password with 2FA and WebAuthn
Example project with:
- Email and password authentication
- Password checks with HaveIBeenPwned
- Sign in with passkeys
- Email verification
- 2FA with TOTP
- 2FA recovery codes
- 2FA with passkeys and security keys
- Password reset with 2FA
- Login throttling and rate limiting
## GitHub repositories
- [Astro](https://github.com/lucia-auth/example-astro-email-password-webauthn)
- [Next.js](https://github.com/lucia-auth/example-nextjs-email-password-webauthn)
- [SvelteKit](https://github.com/lucia-auth/example-sveltekit-email-password-webauthn)
================================================
FILE: pages/examples/email-password-2fa.md
================================================
---
title: "Email and password with 2FA"
---
# Email and password with 2FA
Example project with:
- Email and password authentication
- Password check with HaveIBeenPwned
- Email verification
- 2FA with TOTP
- 2FA recovery codes
- Password reset
- Login throttling and rate limiting
## GitHub repositories
- [Astro](https://github.com/lucia-auth/example-astro-email-password-2fa)
- [Next.js](https://github.com/lucia-auth/example-nextjs-email-password-2fa)
- [SvelteKit](https://github.com/lucia-auth/example-sveltekit-email-password-2fa)
================================================
FILE: pages/examples/github-oauth.md
================================================
---
title: "GitHub OAuth"
---
# GitHub OAuth
Basic example project with GitHub OAuth and rate limiting.
## GitHub repositories
- [Astro](https://github.com/lucia-auth/example-astro-github-oauth)
- [Next.js](https://github.com/lucia-auth/example-nextjs-github-oauth)
- [SvelteKit](https://github.com/lucia-auth/example-sveltekit-github-oauth)
================================================
FILE: pages/examples/google-oauth.md
================================================
---
title: "Google OAuth"
---
# Google OAuth
Basic example project with Google OAuth and rate limiting.
## GitHub repositories
- [Astro](https://github.com/lucia-auth/example-astro-google-oauth)
- [Next.js](https://github.com/lucia-auth/example-nextjs-google-oauth)
- [SvelteKit](https://github.com/lucia-auth/example-sveltekit-google-oauth)
================================================
FILE: pages/index.md
================================================
---
title: "Lucia"
---
# Lucia
Lucia is an open source project to provide resources on implementing authentication using JavaScript and TypeScript.
If 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)!
## Implementation notes
- 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.
- 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.
- SQLite is used for SQL queries but the TypeScript code uses a placeholder database client.
## Related projects
- [The Copenhagen Book](https://thecopenhagenbook.com): A free online resource covering the various auth concepts in web applications.
- [Oslo](https://oslojs.dev): Simple, runtime agnostic, and fully-typed packages with minimal dependency for auth and cryptography.
- [Arctic](https://arcticjs.dev): OAuth 2.0 client library with support for 50+ providers.
## Disclaimer
All 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).
Everything else is licensed under the [MIT license](https://github.com/lucia-auth/lucia/blob/main/LICENSE-MIT).
_Copyright © 2024 pilcrow and contributors_
================================================
FILE: pages/lucia-v3/migrate.md
================================================
---
title: "Migrate from Lucia v3"
---
# Migrate from Lucia v3
Lucia v3 has been deprecated. Lucia is now a learning resource for implementing sessions and more.
## Background
We 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.
## Migrating your project
Replacing 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.
If 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).
### Sessions
```ts
function generateSessionId(): string {
const bytes = new Uint8Array(25);
crypto.getRandomValues(bytes);
const token = encodeBase32LowerCaseNoPadding(bytes);
return token;
}
const sessionExpiresInSeconds = 60 * 60 * 24 * 30; // 30 days
export function createSession(dbPool: DBPool, userId: number): Promise<Session> {
const now = new Date();
const sessionId = generateSessionId();
const session: Session = {
id: sessionId,
userId,
expiresAt: new Date(now.getTime() + 1000 * sessionExpiresInSeconds)
};
await executeQuery(
dbPool,
"INSERT INTO user_session (id, user_id, expires_at) VALUES (?, ?, ?)",
[session.id, session.userId, Math.floor(session.expiresAt.getTime() / 1000)]
);
return session;
}
export function validateSession(dbPool: DBPool, sessionId: string): Promise<Session | null> {
const now = Date.now();
// This may be vulnerable to a timing attack where an attacker can measure the response times
// to guess a valid session ID.
// A more common pattern is a string comparison against a secret using the === operator.
// The === operator is not constant time and the same can be said about SQL = operators.
// Some remote timing attacks has been proven to be possible but there hasn't been a successful
// recorded attack on real-world applications targeting similar vulnerabilities.
const result = dbPool.executeQuery(
dbPool,
"SELECT id, user_id, expires_at FROM session WHERE id = ?",
[sessionId]
);
if (result.rows.length < 1) {
return null;
}
const row = result.rows[0];
const session: Session = {
id: row[0],
userId: row[1],
expiresAt: new Date(row[2] * 1000)
};
if (now.getTime() >= session.expiresAt.getTime()) {
await executeQuery(dbPool, "DELETE FROM user_session WHERE id = ?", [session.id]);
return null;
}
if (now.getTime() >= session.expiresAt.getTime() - (1000 * sessionExpiresInSeconds) / 2) {
session.expiresAt = new Date(Date.now() + 1000 * sessionExpiresInSeconds);
await executeQuery(dbPool, "UPDATE session SET expires_at = ? WHERE id = ?", [
Math.floor(session.expiresAt.getTime() / 1000),
session.id
]);
}
return session;
}
export async function invalidateSession(dbPool: DBPool, sessionId: string): Promise<void> {
await executeQuery(dbPool, "DELETE FROM user_session WHERE id = ?", [sessionId]);
}
export async function invalidateAllSessions(dbPool: DBPool, userId: number): Promise<void> {
await executeQuery(dbPool, "DELETE FROM user_session WHERE user_id = ?", [userId]);
}
export interface Session {
id: string;
userId: number;
expiresAt: Date;
}
```
### Cookies
Cookies should have the following attributes:
- `HttpOnly`: Cookies are only accessible server-side.
- `SameSite=Lax`: Use Strict for critical websites.
- `Secure`: Cookies can only be sent over HTTPS (should be omitted when testing on localhost).
- `Max-Age` or `Expires`: Must be defined to persist cookies.
- `Path=/`: Cookies can be accessed from all routes.
```ts
export function setSessionCookie(response: HTTPResponse, sessionId: string, expiresAt: Date): void {
if (env === ENV.PROD) {
response.headers.add(
"Set-Cookie",
`session=${sessionId}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure;`
);
} else {
response.headers.add(
"Set-Cookie",
`session=${sessionId}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/`
);
}
}
// Set empty session cookie that expires immediately.
export function deleteSessionCookie(response: HTTPResponse): void {
if (env === ENV.PROD) {
response.headers.add(
"Set-Cookie",
"session=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure;"
);
} else {
response.headers.add("Set-Cookie", "session=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/");
}
}
```
================================================
FILE: pages/rate-limit/token-bucket.md
================================================
---
title: "Token bucket"
---
# Token bucket
Each 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.
## Memory storage
This requires the server to persist its memory across requests and will not work in serverless environments.
```ts
export class TokenBucketRateLimit<_Key> {
public max: number;
public refillIntervalSeconds: number;
constructor(max: number, refillIntervalSeconds: number) {
this.max = max;
this.refillIntervalSeconds = refillIntervalSeconds;
}
private storage = new Map<_Key, Bucket>();
public consume(key: _Key, cost: number): boolean {
let bucket = this.storage.get(key) ?? null;
const now = Date.now();
if (bucket === null) {
bucket = {
count: this.max - cost,
refilledAtMilliseconds: now
};
this.storage.set(key, bucket);
return true;
}
const refill = Math.floor(
(now - bucket.refilledAtMilliseconds) / (this.refillIntervalSeconds * 1000)
);
bucket.count = Math.min(bucket.count + refill, this.max);
bucket.refilledAtMilliseconds =
bucket.refilledAtMilliseconds + refill * this.refillIntervalSeconds * 1000;
if (bucket.count < cost) {
this.storage.set(key, bucket);
return false;
}
bucket.count -= cost;
this.storage.set(key, bucket);
return true;
}
}
interface Bucket {
count: number;
refilledAtMilliseconds: number;
}
```
```ts
// Bucket that has 10 tokens max and refills at a rate of 30 seconds/token
const ratelimit = new TokenBucketRateLimit<string>(5, 30);
const valid = ratelimit.consume(ip, 1);
if (!valid) {
throw new Error("Too many requests");
}
```
## Redis
We'll use Lua scripts to ensure queries are atomic.
```lua
-- Returns 1 if allowed, 0 if not
local key = KEYS[1]
local max = tonumber(ARGV[1])
local refillIntervalSeconds = tonumber(ARGV[2])
local cost = tonumber(ARGV[3])
local nowMilliseconds = tonumber(ARGV[4]) -- Current unix time in ms
local fields = redis.call("HGETALL", key)
if #fields == 0 then
local expiresInSeconds = cost * refillIntervalSeconds
redis.call("HSET", key, "count", max - cost, "refilled_at_ms", nowMilliseconds)
redis.call("EXPIRE", key, expiresInSeconds)
return {1}
end
local count = 0
local refilledAtMilliseconds = 0
for i = 1, #fields, 2 do
if fields[i] == "count" then
count = tonumber(fields[i+1])
elseif fields[i] == "refilled_at_ms" then
refilledAtMilliseconds = tonumber(fields[i+1])
end
end
local refill = math.floor((now - refilledAtMilliseconds) / (refillIntervalSeconds * 1000))
count = math.min(count + refill, max)
refilledAtMilliseconds = refilledAtMilliseconds + refill * refillIntervalSeconds * 1000
if count < cost then
return {0}
end
count = count - cost
local expiresInSeconds = (max - count) * refillIntervalSeconds
redis.call("HSET", key, "count", count, "refilled_at_ms", refilledAtMilliseconds)
redis.call("EXPIRE", key, expiresInSeconds)
return {1}
```
Load the script and retrieve the script hash.
```ts
const SCRIPT_SHA = await client.scriptLoad(script);
```
Reference the script with the hash.
```ts
export class TokenBucketRateLimit {
private storageKey: string;
public max: number;
public refillIntervalSeconds: number;
constructor(storageKey: string, max: number, refillIntervalSeconds: number) {
this.storageKey = storageKey;
this.max = max;
this.refillIntervalSeconds = refillIntervalSeconds;
}
public async consume(key: string, cost: number): Promise<boolean> {
const key = `token_bucket.v1:${this.storageKey}:${refillIntervalSeconds}:${key}`;
const result = await client.EVALSHA(SCRIPT_SHA, {
keys: [key],
arguments: [
this.max.toString(),
this.refillIntervalSeconds.toString(),
cost.toString(),
Date.now().toString()
]
});
return Boolean(result[0]);
}
}
```
```ts
// Bucket that has 10 tokens max and refills at a rate of 30 seconds/token
const ratelimit = new TokenBucketRateLimit<string>("ip", 5, 30);
const valid = await ratelimit.consume(ip, 1);
if (!valid) {
throw new Error("Too many requests");
}
```
================================================
FILE: pages/sessions/basic.md
================================================
---
title: "Basic session implementation"
---
# Basic session implementation
## Overview
Sessions 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.
```ts
interface Session {
id: string;
secretHash: Uint8Array; // Uint8Array is a byte array
createdAt: Date;
}
```
Tokens issued to clients include both the ID and un-hashed secret.
```
<SESSION_ID>.<SESSION_SECRET>
```
## Database
The 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.
```
CREATE TABLE session (
id TEXT NOT NULL PRIMARY KEY,
secret_hash BLOB NOT NULL, -- blob is a SQLite data type for raw binary
created_at INTEGER NOT NULL -- unix time (seconds)
) STRICT;
```
> `STRICT` is an SQLite-specific feature that prevents type coercion.
## Generating IDs and secrets
We can generate IDs and secrets by generating a random byte array and encoding it into a string.
For 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.
Since 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.**
```ts
function generateSecureRandomString(): string {
// Human readable alphabet (a-z, 0-9 without l, o, 0, 1 to avoid confusion)
const alphabet = "abcdefghijkmnpqrstuvwxyz23456789";
// Generate 24 bytes = 192 bits of entropy.
// We're only going to use 5 bits per byte so the total entropy will be 192 * 5 / 8 = 120 bits
const bytes = new Uint8Array(24);
crypto.getRandomValues(bytes);
let id = "";
for (let i = 0; i < bytes.length; i++) {
// >> 3 "removes" the right-most 3 bits of the byte
id += alphabet[bytes[i] >> 3];
}
return id;
}
```
> This encoder wastes 3/8 of the random bits. You can optimize it and get better performance by using all the generated random bits.
## Creating sessions
The 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.
```ts
async function createSession(dbPool: DBPool): Promise<SessionWithToken> {
const now = new Date();
const id = generateSecureRandomString();
const secret = generateSecureRandomString();
const secretHash = await hashSecret(secret);
const token = id + "." + secret;
const session: SessionWithToken = {
id,
secretHash,
createdAt: now,
token
};
await executeQuery(dbPool, "INSERT INTO session (id, secret_hash, created_at) VALUES (?, ?, ?)", [
session.id,
session.secretHash,
Math.floor(session.createdAt.getTime() / 1000)
]);
return session;
}
async function hashSecret(secret: string): Promise<Uint8Array> {
const secretBytes = new TextEncoder().encode(secret);
const secretHashBuffer = await crypto.subtle.digest("SHA-256", secretBytes);
return new Uint8Array(secretHashBuffer);
}
interface SessionWithToken extends Session {
token: string;
}
interface Session {
// ...
}
```
## Validating session tokens
To 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.
We recommend setting an expiration for all sessions. Implement an [inactivity timeout](/sessions/inactivity-timeout) instead if you want to keep active users signed in.
```ts
const sessionExpiresInSeconds = 60 * 60 * 24; // 1 day
async function createSession(dbPool: DBPool): Promise<SessionWithToken> {
// ...
}
async function validateSessionToken(dbPool: DBPool, token: string): Promise<Session | null> {
const tokenParts = token.split(".");
if (tokenParts.length !== 2) {
return null;
}
const sessionId = tokenParts[0];
const sessionSecret = tokenParts[1];
const session = await getSession(dbPool, sessionId);
if (!session) {
return null;
}
const tokenSecretHash = await hashSecret(sessionSecret);
const validSecret = constantTimeEqual(tokenSecretHash, session.secretHash);
if (!validSecret) {
return null;
}
return session;
}
async function getSession(dbPool: DBPool, sessionId: string): Promise<Session | null> {
const now = new Date();
const result = await executeQuery(
dbPool,
"SELECT id, secret_hash, created_at FROM session WHERE id = ?",
[sessionId]
);
if (result.rows.length !== 1) {
return null;
}
const row = result.rows[0];
const session: Session = {
id: row[0],
secretHash: row[1],
createdAt: new Date(row[2] * 1000)
};
// Check expiration
if (now.getTime() - session.createdAt.getTime() >= sessionExpiresInSeconds * 1000) {
await deleteSession(sessionId);
return null;
}
return session;
}
async function deleteSession(dbPool: DBPool, sessionId: string): Promise<void> {
await executeQuery(dbPool, "DELETE FROM session WHERE id = ?", [sessionId]);
}
async function hashSecret(secret: string): Promise<Uint8Array> {
// ...
}
interface SessionWithToken extends Session {
// ...
}
interface Session {
// ...
}
```
```ts
function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
if (a.byteLength !== b.byteLength) {
return false;
}
let c = 0;
for (let i = 0; i < a.byteLength; i++) {
c |= a[i] ^ b[i];
}
return c === 0;
}
```
## Client-side storage
For 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.
Cookies usually have a maximum lifetime of 400 days. If you want a persistent session, set a new cookie periodically.
```
Set-Cookie: session_token=SESSION_TOKEN; Max-Age=86400; HttpOnly; Secure; Path=/; SameSite=Lax
```
If 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.
For native applications, use the device's built-in secure storage.
## Securing secret hashes
If 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.
```ts
function encodeSessionPublicJSON(session: Session): string {
// Omit Session.secretHash
const json = JSON.stringify({
id: session.id,
created_at: Math.floor(session.createdAt.getTime() / 1000)
});
return json;
}
```
## CSRF protection
Cross-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.
While 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.
For 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+).
```ts
function verifyRequestOrigin(method: string, originHeader: string): boolean {
if (method === "GET" || method === "HEAD") {
return true;
}
return originHeader === "example.com";
}
// Enable strict origin check only on production environments.
function verifyRequestOrigin(method: string, originHeader: string): boolean {
if (env !== ENV.PROD) {
return true;
}
if (method === "GET" || method === "HEAD") {
return true;
}
return originHeader === "example.com";
}
```
To 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.
================================================
FILE: pages/sessions/frameworks/index.md
================================================
---
title: "Framework-specific implementation notes"
---
# Framework-specific implementation notes
These pages cover some implementation notes for specific frameworks.
- [Next.js](/sessions/frameworks/nextjs)
- [Sveltekit](/sessions/frameworks/sveltekit)
================================================
FILE: pages/sessions/frameworks/nextjs.md
================================================
---
title: "Next.js implementation notes"
---
# Next.js implementation notes
## Validating sessions
We 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.
```ts
import { cookies } from "next/headers";
import { cache } from "react";
export const getCurrentSession = cache(async (): Promise<Session> => {
const cookieStore = await cookies();
const token = cookieStore.get("session")?.value ?? null;
if (token === null) {
return { session: null, user: null };
}
const result = await validateSessionToken(token);
return result;
});
```
## Persisting cookies
You cannot set cookies with `cookies()` or `headers()` when rendering routes. This will throw an error:
```tsx
import { cookies } from "next/headers";
export default function Page() {
const cookieStore = await cookies();
cookieStore.set("message", "hello");
// ...
}
```
This 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.
```ts
export function middleware(request: NextRequest) {
const response = NextResponse.next();
const sessionToken = request.cookies.get("session")?.value ?? null;
if (sessionToken !== null) {
// Re-set the cookie with updated expiration
response.cookies.set({
name: "session",
value: sessionToken,
maxAge: 60 * 60 * 24 * 365, // 1 year
path: "/",
httpOnly: true,
secure: process.env.NODE_ENV === "production"
});
}
return response;
}
```
================================================
FILE: pages/sessions/frameworks/sveltekit.md
================================================
---
title: "SvelteKit implementation notes"
---
# SvelteKit implementation notes
## Authorization check with layouts
A 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.
```
routes/
+layout.server.ts
+page.svelte
foo/
+page.svelte
```
As 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.
================================================
FILE: pages/sessions/inactivity-timeout.md
================================================
---
title: "Inactivity timeout"
---
# Inactivity timeout
This page builds upon the [Basic session implementation](/sessions/basic) page.
Setting 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.
First add a `lastVerifiedAt` attribute to your sessions. This would include when the session token was last verified.
```ts
interface Session {
id: string;
secretHash: Uint8Array;
lastVerifiedAt: Date;
createdAt: Date;
}
```
```
CREATE TABLE session (
id TEXT NOT NULL PRIMARY KEY,
secret_hash BLOB NOT NULL,
last_verified_at INTEGER NOT NULL, -- unix (seconds)
created_at INTEGER NOT NULL,
) STRICT;
```
While 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.
Finally, 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.
```ts
const inactivityTimeoutSeconds = 60 * 60 * 24 * 10; // 10 days
const activityCheckIntervalSeconds = 60 * 60; // 1 hour
async function validateSessionToken(dbPool: DBPool, token: string): Promise<Session | null> {
const now = new Date();
const tokenParts = token.split(".");
if (tokenParts.length !== 2) {
return null;
}
const sessionId = tokenParts[0];
const sessionSecret = tokenParts[1];
const session = await getSession(dbPool, sessionId);
if (!session) {
return null;
}
const tokenSecretHash = await hashSecret(sessionSecret);
const validSecret = constantTimeEqual(tokenSecretHash, session.secretHash);
if (!validSecret) {
return null;
}
if (now.getTime() - session.lastVerifiedAt.getTime() >= activityCheckIntervalSeconds * 1000) {
session.lastVerifiedAt = now;
await executeQuery(dbPool, "UPDATE session SET last_verified_at = ? WHERE id = ?", [
Math.floor(session.lastVerifiedAt.getTime() / 1000),
sessionId
]);
}
return session;
}
async function getSession(dbPool: DBPool, sessionId: string): Promise<Session | null> {
const now = new Date();
const result = await executeQuery(
dbPool,
"SELECT id, secret_hash, last_verified_at, created_at FROM session WHERE id = ?",
[sessionId]
);
if (result.rows.length !== 1) {
return null;
}
const row = result.rows[0];
const session: Session = {
id: row[0],
secretHash: row[1],
lastVerifiedAt: new Date(row[2] * 1000),
createdAt: new Date(row[3] * 1000)
};
// Inactivity timeout
if (now.getTime() - session.lastVerifiedAt.getTime() >= inactivityTimeoutSeconds * 1000) {
await deleteSession(dbPool, sessionId);
return null;
}
return session;
}
```
================================================
FILE: pages/sessions/overview.md
================================================
---
title: "Sessions"
---
# Sessions
HTTP is by design a stateless protocol. The server doesn't know if 2 requests came from the same client.
Browsers 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?
This 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.
Learn 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.
================================================
FILE: pages/sessions/stateless-tokens.md
================================================
---
title: "Stateless tokens"
---
# Stateless tokens
This page builds upon the [Basic session implementation](/sessions/basic) page.
Stateless 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:
```json
{
"session": {
"id": "SESSION_ID",
"created_at": 946684800 // unix (seconds)
},
"iat": 946684800,
"exp": 946684860
}
```
However, 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.
```ts
const session = await createSession();
const sessionJWT = await createSessionJWT(session);
```
To validate sessions, first validate the JWT and validate the session token if the JWT is invalid.
```ts
let sessionToken: string;
let sessionJWT: string;
let validatedSession = await validateSessionJWT(sessionJWT);
// If jwt is invalid/expired, check the main session token.
if (validatedSession === null) {
validatedSession = await validateSessionToken(sessionToken);
}
if (validatedSession === null) {
// no session
}
```
Here 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.
> [`jose`](https://github.com/panva/jose) and [`jsonwebtoken`](https://github.com/auth0/node-jsonwebtoken) are popular NPM packages for creating and validating JWTs.
```ts
import * as oslo_encoding from "@oslojs/encoding";
// Randomly generated key
// For HMAC with SHA-256, the key must be 32 bytes
const jwtHS256Key = new Uint8Array(32);
async function createSessionJWT(session: Session): Promise<string> {
const now = new Date();
const expirationSeconds = 60; // 1 minute
const headerJSON = JSON.stringify({ alg: "HS256", typ: "JWT" });
const headerJSONBytes = new TextEncoder().encode(headerJSON);
const encodedHeader = oslo_encoding.encodeBase64url(headerJSONBytes);
const bodyJSON = JSON.stringify({
// Omit the secret hash
session: {
id: session.id,
created_at: Math.floor(session.createdAt.getTime() / 1000)
},
iat: Math.floor(now.getTime() / 1000),
exp: Math.floor(now.getTime() / 1000) + expirationSeconds
});
const bodyJSONBytes = new TextEncoder().encode(bodyJSON);
const encodedBody = oslo_encoding.encodeBase64url(bodyJSONBytes);
const headerAndBody = encodedHeader + "." + encodedBody;
const headerAndBodyBytes = new TextEncoder().encode(headerAndBody);
const hmacCryptoKey = await crypto.subtle.importKey(
"raw",
jwtHS256Key,
{
name: "HMAC",
hash: "SHA-256"
},
false
);
const signature = await crypto.subtle.sign("HMAC", hmacCryptoKey, headerAndBodyBytes);
const encodedSignature = oslo_jwt.encodeJWT(headerJSON, bodyJSON);
const jw = headerAndBody + "." + encodedSignature;
return jwt;
}
async function validateSessionJWT(jwt: string): Promise<ValidatedSession | null> {
const now = new Date();
const parts = jwt.split(".");
if (parts.length !== 3) {
return null;
}
// Parse header
let header: object;
try {
const headerJSONBytes = oslo_encoding.decodeBase64url(parts[0]);
const headerJSON = new TextDecoder().decode(headerJSONBytes);
const parsedHeader = JSON.parse(headerJSON) as unknown;
if (typeof parsedHeader !== "object" || parsedHeader === null) {
return null;
}
header = parsedHeader;
} catch {
return null;
}
// Verify header claims
if ("typ" in header && header.typ !== "JWT") {
return null;
}
if (!("alg" in header) || header.alg !== "HS256") {
return null;
}
// Verify signature
const signature = oslo_encoding.decodeBase64url(parts[2]);
const headerAndBodyBytes = new TextEncoder().encode(parts[0] + "." + parts[1]);
const hmacCryptoKey = await crypto.subtle.importKey(
"raw",
jwtHS256Key,
{
name: "HMAC",
hash: "SHA-256"
},
false
);
const validSignature = await crypto.subtle.verify(
"HMAC",
hmacCryptoKey,
signature,
headerAndBodyBytes
);
if (!validSignature) {
return null;
}
// Parse body
let body: object;
try {
const bodyJSONParts = oslo_encoding.decodeBase64url(parts[1]);
const bodyJSON = new TextDecoder().decode(bodyJSONParts);
const parsedBody = JSON.parse(bodyJSON) as unknown;
if (typeof parsedBody !== "object" || parsedBody === null) {
return null;
}
body = parsedBody;
} catch {
return null;
}
// Check expiration
if (!("exp" in body) || typeof body.exp !== "number") {
return null;
}
const expiresAt = new Date(body.exp * 1000);
if (now.getTime() >= expiresAt.getTime()) {
return null;
}
// Parse session
if (!("session" in body) || typeof body.session !== "object" || body.session === null) {
return null;
}
const parsedSession = body.session;
if (!("id" in parsedSession) || typeof parsedSession.id !== "string") {
return null;
}
if (!("created_at" in parsedSession) || typeof parsedSession.created_at !== "number") {
return null;
}
const session: ValidatedSession = {
id: parsedSession.id,
createdAt: new Date(parsedSession.created_at * 1000)
};
return session;
}
interface ValidatedSession {
id: string;
createdAt: Date;
}
```
================================================
FILE: pages/tutorials/github-oauth/astro.md
================================================
---
title: "Tutorial: GitHub OAuth in Astro"
---
# Tutorial: GitHub OAuth in Astro
_Before starting, make sure you've created the session and cookie API outlined in the [Sessions](/sessions/overview) page._
An [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).
```
git clone git@github.com:lucia-auth/example-astro-github-oauth.git
```
## Create an OAuth App
[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.
```bash
# .env
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
```
## Update database
Update your user model to include the user's GitHub ID and username.
```ts
interface User {
id: number;
githubId: number;
username: string;
}
```
## Setup Arctic
We recommend using [Arctic](https://arcticjs.dev) for implementing OAuth. Arctic is a lightweight OAuth client library that supports 50+ providers out of the box.
```
npm install arctic
```
Initialize the GitHub provider with the client ID and secret.
```ts
import { GitHub } from "arctic";
export const github = new GitHub(
import.meta.env.GITHUB_CLIENT_ID,
import.meta.env.GITHUB_CLIENT_SECRET,
null
);
```
## Sign in page
Create `pages/login/index.astro` and add a basic sign in button, which should be a link to `/login/github`.
```html
<!-- pages/login/index.astro -->
<html lang="en">
<body>
<h1>Sign in</h1>
<a href="/login/github">Sign in with GitHub</a>
</body>
</html>
```
## Create authorization URL
Create 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.
```ts
// pages/login/github/index.ts
import { generateState } from "arctic";
import { github } from "@lib/oauth";
import type { APIContext } from "astro";
export async function GET(context: APIContext): Promise<Response> {
const state = generateState();
const url = github.createAuthorizationURL(state, []);
context.cookies.set("github_oauth_state", state, {
path: "/",
secure: import.meta.env.PROD,
httpOnly: true,
maxAge: 60 * 10, // 10 minutes
sameSite: "lax"
});
return context.redirect(url.toString());
}
```
## Validate callback
Create 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.
```ts
// pages/login/github/callback.ts
import { generateSessionToken, createSession, setSessionTokenCookie } from "@lib/session";
import { github } from "@lib/oauth";
import type { APIContext } from "astro";
import type { OAuth2Tokens } from "arctic";
export async function GET(context: APIContext): Promise<Response> {
const code = context.url.searchParams.get("code");
const state = context.url.searchParams.get("state");
const storedState = context.cookies.get("github_oauth_state")?.value ?? null;
if (code === null || state === null || storedState === null) {
return new Response(null, {
status: 400
});
}
if (state !== storedState) {
return new Response(null, {
status: 400
});
}
let tokens: OAuth2Tokens;
try {
tokens = await github.validateAuthorizationCode(code);
} catch (e) {
// Invalid code or client credentials
return new Response(null, {
status: 400
});
}
const githubUserResponse = await fetch("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${tokens.accessToken()}`
}
});
const githubUser = await githubUserResponse.json();
const githubUserId = githubUser.id;
const githubUsername = githubUser.login;
// TODO: Replace this with your own DB query.
const existingUser = await getUserFromGitHubId(githubUserId);
if (existingUser !== null) {
const sessionToken = generateSessionToken();
const session = await createSession(sessionToken, existingUser.id);
setSessionTokenCookie(context, sessionToken, session.expiresAt);
return context.redirect("/");
}
// TODO: Replace this with your own DB query.
const user = await createUser(githubUserId, githubUsername);
const sessionToken = generateSessionToken();
const session = await createSession(sessionToken, user.id);
setSessionTokenCookie(context, sessionToken, session.expiresAt);
return context.redirect("/");
}
```
## Get the current user
If 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`.
```ts
if (Astro.locals.user === null) {
return Astro.redirect("/login");
}
const user = Astro.locals.user;
```
## Sign out
Sign out users by invalidating their session. Make sure to remove the session cookie as well.
```ts
import { invalidateSession, deleteSessionTokenCookie } from "@lib/session";
import type { APIContext } from "astro";
export async function POST(context: APIContext): Promise<Response> {
if (context.locals.session === null) {
return new Response(null, {
status: 401
});
}
await invalidateSession(context.locals.session.id);
deleteSessionTokenCookie(context);
return context.redirect("/login");
}
```
================================================
FILE: pages/tutorials/github-oauth/index.md
================================================
---
title: "Tutorial: GitHub OAuth"
---
# Tutorial: GitHub OAuth
In this tutorial, you'll learn how to authenticate users with GitHub and persist sessions with the API you created.
- [Astro](/tutorials/github-oauth/astro)
- [Next.js](/tutorials/github-oauth/nextjs)
- [SvelteKit](/tutorials/github-oauth/sveltekit)
================================================
FILE: pages/tutorials/github-oauth/nextjs.md
================================================
---
title: "Tutorial: GitHub OAuth in Next.js"
---
# Tutorial: GitHub OAuth in Next.js
_Before starting, make sure you've created the session and cookie API outlined in the [Sessions](/sessions/overview) page._
An [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).
```
git clone git@github.com:lucia-auth/example-nextjs-github-oauth.git
```
## Create an OAuth App
[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.
```bash
# .env
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
```
## Update database
Update your user model to include the user's GitHub ID and username.
```ts
interface User {
id: number;
githubId: number;
username: string;
}
```
## Setup Arctic
We recommend using [Arctic](https://arcticjs.dev) for implementing OAuth. Arctic is a lightweight OAuth client library that supports 50+ providers out of the box.
```
npm install arctic
```
Initialize the GitHub provider with the client ID and secret.
```ts
import { GitHub } from "arctic";
export const github = new GitHub(
process.env.GITHUB_CLIENT_ID,
process.env.GITHUB_CLIENT_SECRET,
null
);
```
## Sign in page
Create `app/login/page.tsx` and add a basic sign in button, which should be a link to `/login/github`.
```tsx
// app/login/page.tsx
export default async function Page() {
return (
<>
<h1>Sign in</h1>
<a href="/login/github">Sign in with GitHub</a>
</>
);
}
```
## Create authorization URL
Create 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.
```ts
// app/login/github/route.ts
import { generateState } from "arctic";
import { github } from "@/lib/oauth";
import { cookies } from "next/headers";
export async function GET(): Promise<Response> {
const state = generateState();
const url = github.createAuthorizationURL(state, []);
const cookieStore = await cookies();
cookieStore.set("github_oauth_state", state, {
path: "/",
secure: process.env.NODE_ENV === "production",
httpOnly: true,
maxAge: 60 * 10,
sameSite: "lax"
});
return new Response(null, {
status: 302,
headers: {
Location: url.toString()
}
});
}
```
## Validate callback
Create 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.
```ts
// app/login/github/callback/route.ts
import { generateSessionToken, createSession, setSessionTokenCookie } from "@/lib/session";
import { github } from "@/lib/oauth";
import { cookies } from "next/headers";
import type { OAuth2Tokens } from "arctic";
export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const cookieStore = await cookies();
const storedState = cookieStore.get("github_oauth_state")?.value ?? null;
if (code === null || state === null || storedState === null) {
return new Response(null, {
status: 400
});
}
if (state !== storedState) {
return new Response(null, {
status: 400
});
}
let tokens: OAuth2Tokens;
try {
tokens = await github.validateAuthorizationCode(code);
} catch (e) {
// Invalid code or client credentials
return new Response(null, {
status: 400
});
}
const githubUserResponse = await fetch("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${tokens.accessToken()}`
}
});
const githubUser = await githubUserResponse.json();
const githubUserId = githubUser.id;
const githubUsername = githubUser.login;
// TODO: Replace this with your own DB query.
const existingUser = await getUserFromGitHubId(githubUserId);
if (existingUser !== null) {
const sessionToken = generateSessionToken();
const session = await createSession(sessionToken, existingUser.id);
await setSessionTokenCookie(sessionToken, session.expiresAt);
return new Response(null, {
status: 302,
headers: {
Location: "/"
}
});
}
// TODO: Replace this with your own DB query.
const user = await createUser(githubUserId, githubUsername);
const sessionToken = generateSessionToken();
const session = await createSession(sessionToken, user.id);
await setSessionTokenCookie(sessionToken, session.expiresAt);
return new Response(null, {
status: 302,
headers: {
Location: "/"
}
});
}
```
## Validate requests
Use the `getCurrentSession()` function from the [Session cookies in Next.js](/sessions/cookies/nextjs) page to get the current user and session.
```tsx
import { redirect } from "next/navigation";
import { getCurrentSession } from "@/lib/session";
export default async function Page() {
const { user } = await getCurrentSession();
if (user === null) {
return redirect("/login");
}
return <h1>Hi, {user.username}!</h1>;
}
```
## Sign out
Sign out users by invalidating their session. Make sure to remove the session cookie as well.
```tsx
import { getCurrentSession, invalidateSession, deleteSessionTokenCookie } from "@/lib/session";
import { redirect } from "next/navigation";
import { cookies } from "next/headers";
export default async function Page() {
return (
<form action={logout}>
<button>Sign out</button>
</form>
);
}
async function logout(): Promise<ActionResult> {
"use server";
const { session } = await getCurrentSession();
if (!session) {
return {
error: "Unauthorized"
};
}
await invalidateSession(session.id);
await deleteSessionTokenCookie();
return redirect("/login");
}
interface ActionResult {
error: string | null;
}
```
================================================
FILE: pages/tutorials/github-oauth/sveltekit.md
================================================
---
title: "Tutorial: GitHub OAuth in SvelteKit"
---
# Tutorial: GitHub OAuth in SvelteKit
_Before starting, make sure you've created the session and cookie API outlined in the [Sessions](/sessions/overview) page._
An [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).
```
git clone git@github.com:lucia-auth/example-sveltekit-github-oauth.git
```
## Create an OAuth App
[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.
```bash
# .env
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
```
## Update database
Update your user model to include the user's GitHub ID and username.
```ts
interface User {
id: number;
githubId: number;
username: string;
}
```
## Setup Arctic
We recommend using [Arctic](https://arcticjs.dev) for implementing OAuth. Arctic is a lightweight OAuth client library that supports 50+ providers out of the box.
```
npm install arctic
```
Initialize the GitHub provider with the client ID and secret.
```ts
import { GitHub } from "arctic";
import { GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET } from "$env/static/private";
export const github = new GitHub(GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, null);
```
## Sign in page
Create `routes/login/+page.svelte` and add a basic sign in button, which should be a link to `/login/github`.
```svelte
<!-- routes/login/+page.svelte -->
<h1>Sign in</h1>
<a href="/login/github">Sign in with GitHub</a>
```
## Create authorization URL
Create 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.
```ts
// routes/login/github/+server.ts
import { generateState } from "arctic";
import { github } from "$lib/server/oauth";
import type { RequestEvent } from "@sveltejs/kit";
export async function GET(event: RequestEvent): Promise<Response> {
const state = generateState();
const url = github.createAuthorizationURL(state, []);
event.cookies.set("github_oauth_state", state, {
path: "/",
httpOnly: true,
maxAge: 60 * 10,
sameSite: "lax"
});
return new Response(null, {
status: 302,
headers: {
Location: url.toString()
}
});
}
```
## Validate callback
Create 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.
```ts
// routes/login/github/callback/+server.ts
import { generateSessionToken, createSession, setSessionTokenCookie } from "$lib/server/session";
import { github } from "$lib/server/oauth";
import type { RequestEvent } from "@sveltejs/kit";
import type { OAuth2Tokens } from "arctic";
export async function GET(event: RequestEvent): Promise<Response> {
const code = event.url.searchParams.get("code");
const state = event.url.searchParams.get("state");
const storedState = event.cookies.get("github_oauth_state") ?? null;
if (code === null || state === null || storedState === null) {
return new Response(null, {
status: 400
});
}
if (state !== storedState) {
return new Response(null, {
status: 400
});
}
let tokens: OAuth2Tokens;
try {
tokens = await github.validateAuthorizationCode(code);
} catch (e) {
// Invalid code or client credentials
return new Response(null, {
status: 400
});
}
const githubUserResponse = await fetch("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${tokens.accessToken()}`
}
});
const githubUser = await githubUserResponse.json();
const githubUserId = githubUser.id;
const githubUsername = githubUser.login;
// TODO: Replace this with your own DB query.
const existingUser = await getUserFromGitHubId(githubUserId);
if (existingUser) {
const sessionToken = generateSessionToken();
const session = await createSession(sessionToken, existingUser.id);
setSessionTokenCookie(event, sessionToken, session.expiresAt);
return new Response(null, {
status: 302,
headers: {
Location: "/"
}
});
}
// TODO: Replace this with your own DB query.
const user = await createUser(githubUserId, githubUsername);
const sessionToken = generateSessionToken();
const session = await createSession(sessionToken, user.id);
setSessionTokenCookie(event, sessionToken, session.expiresAt);
return new Response(null, {
status: 302,
headers: {
Location: "/"
}
});
}
```
## Get the current user
If 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`.
```ts
// routes/+page.server.ts
import { redirect } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async (event) => {
if (!event.locals.user) {
return redirect(302, "/login");
}
return {
user
};
};
```
## Sign out
Sign out users by invalidating their session. Make sure to remove the session cookie as well.
```ts
// routes/+page.server.ts
import { fail, redirect } from "@sveltejs/kit";
import { invalidateSession, deleteSessionTokenCookie } from "$lib/server/session";
import type { Actions, PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ locals }) => {
// ...
};
export const actions: Actions = {
default: async (event) => {
if (event.locals.session === null) {
return fail(401);
}
await invalidateSession(event.locals.session.id);
deleteSessionTokenCookie(event);
return redirect(302, "/login");
}
};
```
```svelte
<!-- routes/+page.svelte -->
<script lang="ts">
import { enhance } from "$app/forms";
</script>
<form method="post" use:enhance>
<button>Sign out</button>
</form>
```
================================================
FILE: pages/tutorials/google-oauth/astro.md
================================================
---
title: "Tutorial: Google OAuth in Astro"
---
# Tutorial: Google OAuth in Astro
_Before starting, make sure you've created the session and cookie API outlined in the [Sessions](/sessions/overview) page._
An [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).
```
git clone git@github.com:lucia-auth/example-astro-google-oauth.git
```
## Create an OAuth App
Create 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.
```bash
# .env
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
```
## Update database
Update your user model to include the user's Google ID and username.
```ts
interface User {
id: number;
googleId: string;
name: string;
}
```
## Setup Arctic
We recommend using [Arctic](https://arcticjs.dev) for implementing OAuth. Arctic is a lightweight OAuth client library that supports 50+ providers out of the box.
```
npm install arctic
```
Initialize the Google provider with the client ID and secret.
```ts
import { Google } from "arctic";
export const google = new Google(
import.meta.env.GOOGLE_CLIENT_ID,
import.meta.env.GOOGLE_CLIENT_SECRET,
"http://localhost:4321/login/google/callback"
);
```
## Sign in page
Create `pages/login/index.astro` and add a basic sign in button, which should be a link to `/login/google`.
```html
<!-- pages/login/index.astro -->
<html lang="en">
<body>
<h1>Sign in</h1>
<a href="/login/google">Sign in with Google</a>
</body>
</html>
```
## Create authorization URL
Create 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.
```ts
// pages/login/google/index.ts
import { generateState } from "arctic";
import { google } from "@lib/oauth";
import type { APIContext } from "astro";
export async function GET(context: APIContext): Promise<Response> {
const state = generateState();
const codeVerifier = generateCodeVerifier();
const url = google.createAuthorizationURL(state, codeVerifier, ["openid", "profile"]);
context.cookies.set("google_oauth_state", state, {
path: "/",
secure: import.meta.env.PROD,
httpOnly: true,
maxAge: 60 * 10, // 10 minutes
sameSite: "lax"
});
context.cookies.set("google_code_verifier", codeVerifier, {
path: "/",
secure: import.meta.env.PROD,
httpOnly: true,
maxAge: 60 * 10, // 10 minutes
sameSite: "lax"
});
return context.redirect(url.toString());
}
```
## Validate callback
Create 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.
```ts
// pages/login/google/callback.ts
import { generateSessionToken, createSession, setSessionTokenCookie } from "@lib/server/session";
import { google } from "@lib/oauth";
import { decodeIdToken } from "arctic";
import type { APIContext } from "astro";
import type { OAuth2Tokens } from "arctic";
export async function GET(context: APIContext): Promise<Response> {
const code = context.url.searchParams.get("code");
const state = context.url.searchParams.get("state");
const storedState = context.cookies.get("google_oauth_state")?.value ?? null;
const codeVerifier = context.cookies.get("google_code_verifier")?.value ?? null;
if (code === null || state === null || storedState === null || codeVerifier === null) {
return new Response(null, {
status: 400
});
}
if (state !== storedState) {
return new Response(null, {
status: 400
});
}
let tokens: OAuth2Tokens;
try {
tokens = await google.validateAuthorizationCode(code, codeVerifier);
} catch (e) {
// Invalid code or client credentials
return new Response(null, {
status: 400
});
}
const claims = decodeIdToken(tokens.idToken());
const googleUserId = claims.sub;
const username = claims.name;
// TODO: Replace this with your own DB query.
const existingUser = await getUserFromGoogleId(googleUserId);
if (existingUser !== null) {
const sessionToken = generateSessionToken();
const session = await createSession(sessionToken, existingUser.id);
setSessionTokenCookie(context, sessionToken, session.expiresAt);
return context.redirect("/");
}
// TODO: Replace this with your own DB query.
const user = await createUser(googleUserId, username);
const sessionToken = generateSessionToken();
const session = await createSession(sessionToken, user.id);
setSessionTokenCookie(context, sessionToken, session.expiresAt);
return context.redirect("/");
}
```
## Get the current user
If 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`.
```ts
if (Astro.locals.user === null) {
return Astro.redirect("/login");
}
const user = Astro.locals.user;
```
## Sign out
Sign out users by invalidating their session. Make sure to remove the session cookie as well.
```ts
import { invalidateSession, deleteSessionTokenCookie } from "@lib/server/session";
import type { APIContext } from "astro";
export async function POST(context: APIContext): Promise<Response> {
if (context.locals.session === null) {
return new Response(null, {
status: 401
});
}
await invalidateSession(context.locals.session.id);
deleteSessionTokenCookie(context);
return context.redirect("/login");
}
```
================================================
FILE: pages/tutorials/google-oauth/index.md
================================================
---
title: "Tutorial: Google OAuth"
---
# Tutorial: Google OAuth
In this tutorial, you'll learn how to authenticate users with Google and persist sessions with the API you created.
- [Astro](/tutorials/google-oauth/astro)
- [Next.js](/tutorials/google-oauth/nextjs)
- [SvelteKit](/tutorials/google-oauth/sveltekit)
================================================
FILE: pages/tutorials/google-oauth/nextjs.md
================================================
---
title: "Tutorial: Google OAuth in Next.js"
---
# Tutorial: Google OAuth in Next.js
_Before starting, make sure you've created the session and cookie API outlined in the [Sessions](/sessions/overview) page._
An [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).
```
git clone git@github.com:lucia-auth/example-nextjs-google-oauth.git
```
## Create an OAuth App
Create 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.
```bash
# .env
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
```
## Update database
Update your user model to include the user's Google ID and username.
```ts
interface User {
id: number;
googleId: string;
name: string;
}
```
## Setup Arctic
```
npm install arctic
```
Initialize the Google provider with the client ID, client secret, and redirect URI.
```ts
import { Google } from "arctic";
export const google = new Google(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
"http://localhost:3000/login/google/callback"
);
```
## Sign in page
Create `app/login/page.tsx` and add a basic sign in button, which should be a link to `/login/google`.
```tsx
// app/login/page.tsx
export default async function Page() {
return (
<>
<h1>Sign in</h1>
<a href="/login/google">Sign in with Google</a>
</>
);
}
```
## Create authorization URL
Create 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.
```ts
// app/login/google/route.ts
import { generateState, generateCodeVerifier } from "arctic";
import { google } from "@/lib/auth";
import { cookies } from "next/headers";
export async function GET(): Promise<Response> {
const state = generateState();
const codeVerifier = generateCodeVerifier();
const url = google.createAuthorizationURL(state, codeVerifier, ["openid", "profile"]);
const cookieStore = await cookies();
cookieStore.set("google_oauth_state", state, {
path: "/",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 10, // 10 minutes
sameSite: "lax"
});
cookieStore.set("google_code_verifier", codeVerifier, {
path: "/",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 10, // 10 minutes
sameSite: "lax"
});
return new Response(null, {
status: 302,
headers: {
Location: url.toString()
}
});
}
```
## Validate callback
Create 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.
```ts
// app/login/google/callback/route.ts
import { generateSessionToken, createSession, setSessionTokenCookie } from "@/lib/session";
import { google } from "@/lib/oauth";
import { cookies } from "next/headers";
import { decodeIdToken } from "arctic";
import type { OAuth2Tokens } from "arctic";
export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const cookieStore = await cookies();
const storedState = cookieStore.get("google_oauth_state")?.value ?? null;
const codeVerifier = cookieStore.get("google_code_verifier")?.value ?? null;
if (code === null || state === null || storedState === null || codeVerifier === null) {
return new Response(null, {
status: 400
});
}
if (state !== storedState) {
return new Response(null, {
status: 400
});
}
let tokens: OAuth2Tokens;
try {
tokens = await google.validateAuthorizationCode(code, codeVerifier);
} catch (e) {
// Invalid code or client credentials
return new Response(null, {
status: 400
});
}
const claims = decodeIdToken(tokens.idToken());
const googleUserId = claims.sub;
const username = claims.name;
// TODO: Replace this with your own DB query.
const existingUser = await getUserFromGoogleId(googleUserId);
if (existingUser !== null) {
const sessionToken = generateSessionToken();
const session = await createSession(sessionToken, existingUser.id);
await setSessionTokenCookie(sessionToken, session.expiresAt);
return new Response(null, {
status: 302,
headers: {
Location: "/"
}
});
}
// TODO: Replace this with your own DB query.
const user = await createUser(googleUserId, username);
const sessionToken = generateSessionToken();
const session = await createSession(sessionToken, user.id);
await setSessionTokenCookie(sessionToken, session.expiresAt);
return new Response(null, {
status: 302,
headers: {
Location: "/"
}
});
}
```
## Validate requests
Use the `getCurrentSession()` function from the [Session cookies in Next.js](/sessions/cookies/nextjs) page to get the current user and session.
```tsx
import { redirect } from "next/navigation";
import { getCurrentSession } from "@/lib/session";
export default async function Page() {
const { user } = await getCurrentSession();
if (user === null) {
return redirect("/login");
}
return <h1>Hi, {user.name}!</h1>;
}
```
## Sign out
Sign out users by invalidating their session. Make sure to remove the session cookie as well.
```tsx
import { getCurrentSession, invalidateSession, deleteSessionTokenCookie } from "@/lib/session";
import { redirect } from "next/navigation";
import { cookies } from "next/headers";
export default async function Page() {
return (
<form action={logout}>
<button>Sign out</button>
</form>
);
}
async function logout(): Promise<ActionResult> {
"use server";
const { session } = await getCurrentSession();
if (!session) {
return {
error: "Unauthorized"
};
}
await invalidateSession(session.id);
await deleteSessionTokenCookie();
return redirect("/login");
}
interface ActionResult {
error: string | null;
}
```
================================================
FILE: pages/tutorials/google-oauth/sveltekit.md
================================================
---
title: "Tutorial: Google OAuth in SvelteKit"
---
# Tutorial: Google OAuth in SvelteKit
_Before starting, make sure you've created the session and cookie API outlined in the [Sessions](/sessions/overview) page._
An [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).
```
git clone git@github.com:lucia-auth/example-sveltekit-google-oauth.git
```
## Create an OAuth App
Create 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.
```bash
# .env
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
```
## Update database
Update your user model to include the user's Google ID and username.
```ts
interface User {
id: number;
googleId: string;
name: string;
}
```
## Setup Arctic
```
npm install arctic
```
Initialize the Google provider with the client ID, client secret, and redirect URI.
```ts
import { Google } from "arctic";
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET } from "$env/static/private";
export const google = new Google(
GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET,
"http://localhost:5173/login/google/callback"
);
```
## Sign in page
Create `routes/login/+page.svelte` and add a basic sign in button, which should be a link to `/login/google`.
```svelte
<!-- routes/login/+page.svelte -->
<h1>Sign in</h1>
<a href="/login/google">Sign in with Google</a>
```
## Create authorization URL
Create 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.
```ts
// routes/login/google/+server.ts
import { generateState, generateCodeVerifier } from "arctic";
import { google } from "$lib/server/oauth";
import type { RequestEvent } from "@sveltejs/kit";
export async function GET(event: RequestEvent): Promise<Response> {
const state = generateState();
const codeVerifier = generateCodeVerifier();
const url = google.createAuthorizationURL(state, codeVerifier, ["openid", "profile"]);
event.cookies.set("google_oauth_state", state, {
path: "/",
httpOnly: true,
maxAge: 60 * 10, // 10 minutes
sameSite: "lax"
});
event.cookies.set("google_code_verifier", codeVerifier, {
path: "/",
httpOnly: true,
maxAge: 60 * 10, // 10 minutes
sameSite: "lax"
});
return new Response(null, {
status: 302,
headers: {
Location: url.toString()
}
});
}
```
## Validate callback
Create 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.
```ts
// routes/login/google/callback/+server.ts
import { generateSessionToken, createSession, setSessionTokenCookie } from "$lib/server/session";
import { google } from "$lib/server/oauth";
import { decodeIdToken } from "arctic";
import type { RequestEvent } from "@sveltejs/kit";
import type { OAuth2Tokens } from "arctic";
export async function GET(event: RequestEvent): Promise<Response> {
const code = event.url.searchParams.get("code");
const state = event.url.searchParams.get("state");
const storedState = event.cookies.get("google_oauth_state") ?? null;
const codeVerifier = event.cookies.get("google_code_verifier") ?? null;
if (code === null || state === null || storedState === null || codeVerifier === null) {
return new Response(null, {
status: 400
});
}
if (state !== storedState) {
return new Response(null, {
status: 400
});
}
let tokens: OAuth2Tokens;
try {
tokens = await google.validateAuthorizationCode(code, codeVerifier);
} catch (e) {
// Invalid code or client credentials
return new Response(null, {
status: 400
});
}
const claims = decodeIdToken(tokens.idToken());
const googleUserId = claims.sub;
const username = claims.name;
// TODO: Replace this with your own DB query.
const existingUser = await getUserFromGoogleId(googleUserId);
if (existingUser !== null) {
const sessionToken = generateSessionToken();
const session = await createSession(sessionToken, existingUser.id);
setSessionTokenCookie(event, sessionToken, session.expiresAt);
return new Response(null, {
status: 302,
headers: {
Location: "/"
}
});
}
// TODO: Replace this with your own DB query.
const user = await createUser(googleUserId, username);
const sessionToken = generateSessionToken();
const session = await createSession(sessionToken, user.id);
setSessionTokenCookie(event, sessionToken, session.expiresAt);
return new Response(null, {
status: 302,
headers: {
Location: "/"
}
});
}
```
## Get the current user
If 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`.
```ts
// routes/+page.server.ts
import { redirect } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async (event) => {
if (!event.locals.user) {
return redirect(302, "/login");
}
return {
user
};
};
```
## Sign out
Sign out users by invalidating their session. Make sure to remove the session cookie as well.
```ts
// routes/+page.server.ts
import { fail, redirect } from "@sveltejs/kit";
import { invalidateSession, deleteSessionTokenCookie } from "$lib/server/session";
import type { Actions, PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ locals }) => {
// ...
};
export const actions: Actions = {
default: async (event) => {
if (event.locals.session === null) {
return fail(401);
}
await invalidateSession(event.locals.session.id);
deleteSessionTokenCookie(event);
return redirect(302, "/login");
}
};
```
```svelte
<!-- routes/+page.svelte -->
<script lang="ts">
import { enhance } from "$app/forms";
</script>
<form method="post" use:enhance>
<button>Sign out</button>
</form>
```
gitextract_16u519e7/
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ └── publish.yaml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── LICENSE-0BSD
├── LICENSE-MIT
├── README.md
├── malta.config.json
├── package.json
└── pages/
├── examples/
│ ├── email-password-2fa-webauthn.md
│ ├── email-password-2fa.md
│ ├── github-oauth.md
│ └── google-oauth.md
├── index.md
├── lucia-v3/
│ └── migrate.md
├── rate-limit/
│ └── token-bucket.md
├── sessions/
│ ├── basic.md
│ ├── frameworks/
│ │ ├── index.md
│ │ ├── nextjs.md
│ │ └── sveltekit.md
│ ├── inactivity-timeout.md
│ ├── overview.md
│ └── stateless-tokens.md
└── tutorials/
├── github-oauth/
│ ├── astro.md
│ ├── index.md
│ ├── nextjs.md
│ └── sveltekit.md
└── google-oauth/
├── astro.md
├── index.md
├── nextjs.md
└── sveltekit.md
Condensed preview — 32 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (86K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 23,
"preview": "github: pilcrowOnPaper\n"
},
{
"path": ".github/workflows/publish.yaml",
"chars": 820,
"preview": "name: \"Publish\"\non:\n push:\n branches:\n - main\n\nenv:\n CLOUDFLARE_API_TOKEN: ${{secrets.CLOUDFLARE_PAGES_API_TOK"
},
{
"path": ".gitignore",
"chars": 60,
"preview": "dist\npnpm-lock.yaml\nnode_modules\npackage-lock.json\n.DS_Store"
},
{
"path": ".prettierignore",
"chars": 74,
"preview": ".DS_Store\nnode_modules\n/dist\n\npnpm-lock.yaml\npackage-lock.json\nyarn.lock\n\n"
},
{
"path": ".prettierrc.json",
"chars": 67,
"preview": "{\n\t\"useTabs\": true,\n\t\"trailingComma\": \"none\",\n\t\"printWidth\": 100\n}\n"
},
{
"path": "LICENSE-0BSD",
"chars": 658,
"preview": "Copyright (c) 2024 pilcrowOnPaper and contributors\n\nPermission to use, copy, modify, and/or distribute this software for"
},
{
"path": "LICENSE-MIT",
"chars": 1074,
"preview": "Copyright (c) 2024 pilcrowOnPaper and contributors\n\nPermission is hereby granted, free of charge, to any person obtainin"
},
{
"path": "README.md",
"chars": 2515,
"preview": "# Lucia\n\n**Link: [lucia-auth.com](https://lucia-auth.com)**\n\n> [!IMPORTANT] \n> Lucia v3 will be deprecated by March 202"
},
{
"path": "malta.config.json",
"chars": 1627,
"preview": "{\n\t\"name\": \"Lucia\",\n\t\"description\": \"An open source resource on implementing authentication with JavaScript\",\n\t\"domain\":"
},
{
"path": "package.json",
"chars": 249,
"preview": "{\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://gi"
},
{
"path": "pages/examples/email-password-2fa-webauthn.md",
"chars": 656,
"preview": "---\ntitle: \"Email and password with 2FA and WebAuthn\"\n---\n\n# Email and password with 2FA and WebAuthn\n\nExample project w"
},
{
"path": "pages/examples/email-password-2fa.md",
"chars": 543,
"preview": "---\ntitle: \"Email and password with 2FA\"\n---\n\n# Email and password with 2FA\n\nExample project with:\n\n- Email and password"
},
{
"path": "pages/examples/github-oauth.md",
"chars": 346,
"preview": "---\ntitle: \"GitHub OAuth\"\n---\n\n# GitHub OAuth\n\nBasic example project with GitHub OAuth and rate limiting.\n\n## GitHub rep"
},
{
"path": "pages/examples/google-oauth.md",
"chars": 346,
"preview": "---\ntitle: \"Google OAuth\"\n---\n\n# Google OAuth\n\nBasic example project with Google OAuth and rate limiting.\n\n## GitHub rep"
},
{
"path": "pages/index.md",
"chars": 1984,
"preview": "---\ntitle: \"Lucia\"\n---\n\n# Lucia\n\nLucia is an open source project to provide resources on implementing authentication usi"
},
{
"path": "pages/lucia-v3/migrate.md",
"chars": 4665,
"preview": "---\ntitle: \"Migrate from Lucia v3\"\n---\n\n# Migrate from Lucia v3\n\nLucia v3 has been deprecated. Lucia is now a learning r"
},
{
"path": "pages/rate-limit/token-bucket.md",
"chars": 4291,
"preview": "---\ntitle: \"Token bucket\"\n---\n\n# Token bucket\n\nEach user has their own bucket of tokens that gets refilled at a set inte"
},
{
"path": "pages/sessions/basic.md",
"chars": 8638,
"preview": "---\ntitle: \"Basic session implementation\"\n---\n\n# Basic session implementation\n\n## Overview\n\nSessions have an ID and secr"
},
{
"path": "pages/sessions/frameworks/index.md",
"chars": 258,
"preview": "---\ntitle: \"Framework-specific implementation notes\"\n---\n\n# Framework-specific implementation notes\n\nThese pages cover s"
},
{
"path": "pages/sessions/frameworks/nextjs.md",
"chars": 1604,
"preview": "---\ntitle: \"Next.js implementation notes\"\n---\n\n# Next.js implementation notes\n\n## Validating sessions\n\nWe recommend crea"
},
{
"path": "pages/sessions/frameworks/sveltekit.md",
"chars": 639,
"preview": "---\ntitle: \"SvelteKit implementation notes\"\n---\n\n# SvelteKit implementation notes\n\n## Authorization check with layouts\n\n"
},
{
"path": "pages/sessions/inactivity-timeout.md",
"chars": 3013,
"preview": "---\ntitle: \"Inactivity timeout\"\n---\n\n# Inactivity timeout\n\nThis page builds upon the [Basic session implementation](/ses"
},
{
"path": "pages/sessions/overview.md",
"chars": 1132,
"preview": "---\ntitle: \"Sessions\"\n---\n\n# Sessions\n\nHTTP is by design a stateless protocol. The server doesn't know if 2 requests cam"
},
{
"path": "pages/sessions/stateless-tokens.md",
"chars": 5517,
"preview": "---\ntitle: \"Stateless tokens\"\n---\n\n# Stateless tokens\n\nThis page builds upon the [Basic session implementation](/session"
},
{
"path": "pages/tutorials/github-oauth/astro.md",
"chars": 5722,
"preview": "---\ntitle: \"Tutorial: GitHub OAuth in Astro\"\n---\n\n# Tutorial: GitHub OAuth in Astro\n\n_Before starting, make sure you've "
},
{
"path": "pages/tutorials/github-oauth/index.md",
"chars": 318,
"preview": "---\ntitle: \"Tutorial: GitHub OAuth\"\n---\n\n# Tutorial: GitHub OAuth\n\nIn this tutorial, you'll learn how to authenticate us"
},
{
"path": "pages/tutorials/github-oauth/nextjs.md",
"chars": 6339,
"preview": "---\ntitle: \"Tutorial: GitHub OAuth in Next.js\"\n---\n\n# Tutorial: GitHub OAuth in Next.js\n\n_Before starting, make sure you"
},
{
"path": "pages/tutorials/github-oauth/sveltekit.md",
"chars": 6380,
"preview": "---\ntitle: \"Tutorial: GitHub OAuth in SvelteKit\"\n---\n\n# Tutorial: GitHub OAuth in SvelteKit\n\n_Before starting, make sure"
},
{
"path": "pages/tutorials/google-oauth/astro.md",
"chars": 6117,
"preview": "---\ntitle: \"Tutorial: Google OAuth in Astro\"\n---\n\n# Tutorial: Google OAuth in Astro\n\n_Before starting, make sure you've "
},
{
"path": "pages/tutorials/google-oauth/index.md",
"chars": 318,
"preview": "---\ntitle: \"Tutorial: Google OAuth\"\n---\n\n# Tutorial: Google OAuth\n\nIn this tutorial, you'll learn how to authenticate us"
},
{
"path": "pages/tutorials/google-oauth/nextjs.md",
"chars": 6613,
"preview": "---\ntitle: \"Tutorial: Google OAuth in Next.js\"\n---\n\n# Tutorial: Google OAuth in Next.js\n\n_Before starting, make sure you"
},
{
"path": "pages/tutorials/google-oauth/sveltekit.md",
"chars": 6625,
"preview": "---\ntitle: \"Tutorial: Google OAuth in SvelteKit\"\n---\n\n# Tutorial: Google OAuth in SvelteKit\n\n_Before starting, make sure"
}
]
About this extraction
This page contains the full source code of the lucia-auth/lucia GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 32 files (77.4 KB), approximately 20.2k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.