diff --git a/docs/pages/translations/ja/basics/configuration.md b/docs/pages/translations/ja/basics/configuration.md new file mode 100644 index 000000000..fcd2f36e7 --- /dev/null +++ b/docs/pages/translations/ja/basics/configuration.md @@ -0,0 +1,104 @@ +--- +title: "Configuration" +--- + +# Configuration + +This page shows all the options for [`Lucia`](/reference/main/Lucia) to configure Lucia. + +```ts +interface Options { + sessionExpiresIn?: TimeSpan; + sessionCookie?: SessionCookieOptions; + getSessionAttributes?: ( + databaseSessionAttributes: DatabaseSessionAttributes + ) => _SessionAttributes; + getUserAttributes?: (databaseUserAttributes: DatabaseUserAttributes) => _UserAttributes; +} +``` + +## `sessionExpiresIn` + +Configures how long a session stays valid for inactive users. Session expirations are automatically extended for active users. Also see [`TimeSpan`](/reference/main/TimeSpan). + +```ts +import { Lucia, TimeSpan } from "lucia"; + +const lucia = new Lucia(adapter, { + sessionExpiresIn: new TimeSpan(2, "w") +}); +``` + +## `sessionCookie` + +Configures the session cookie. + +```ts +import { Lucia } from "lucia"; + +const lucia = new Lucia(adapter, { + sessionCookie: { + name: "session", + expires: false, // session cookies have very long lifespan (2 years) + attributes: { + secure: true, + sameSite: "strict", + domain: "example.com" + } + } +}); +``` + +## `getSessionAttributes()` + +Transforms database session attributes, which is typed as `DatabaseSessionAttributes`. The returned object is added to the `Session` object. + +```ts +import { Lucia } from "lucia"; + +const lucia = new Lucia(adapter, { + getSessionAttributes: (attributes) => { + return { + country: attributes.country + }; + } +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseSessionAttributes: DatabaseSessionAttributes; + } +} + +interface DatabaseSessionAttributes { + country: string; +} +``` + +## `getUserAttributes()` + +Transforms database user attributes, which is typed as `DatabaseUserAttributes`. The returned object is added to the `User` object. + +```ts +import { Lucia } from "lucia"; + +const lucia = new Lucia(adapter, { + getUserAttributes: (attributes) => { + return { + username: attributes.username + }; + } +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: DatabaseUserAttributes; + } +} + +interface DatabaseUserAttributes { + username: string; +} +``` diff --git a/docs/pages/translations/ja/basics/sessions.md b/docs/pages/translations/ja/basics/sessions.md new file mode 100644 index 000000000..3631282a3 --- /dev/null +++ b/docs/pages/translations/ja/basics/sessions.md @@ -0,0 +1,177 @@ +--- +title: "Sessions" +--- + +# Sessions + +Sessions allow Lucia to keep track of requests made by authenticated users. The ID can be stored in a cookie or used as a traditional token manually added to each request. They should be created and stored on registration and login, validated on every request, and deleted on sign out. + +```ts +interface Session extends SessionAttributes { + id: string; + userId: string; + expiresAt: Date; + fresh: boolean; +} +``` + +## Session lifetime + +Sessions do not have an absolute expiration. The expiration gets extended whenever they're used. This ensures that active users remain signed in, while inactive users are signed out. + +More specifically, if the session expiration is set to 30 days (default), Lucia will extend the expiration by another 30 days when there are less than 15 days (half of the expiration) until expiration. You can configure the expiration with the `sessionExpiresIn` configuration. + +```ts +import { Lucia, TimeSpan } from "lucia"; + +export const lucia = new Lucia(adapter, { + sessionExpiresIn: new TimeSpan(2, "w") // 2 weeks +}); +``` + +## Define session attributes + +Defining custom session attributes requires 2 steps. First, add the required columns to the session table. You can type it by declaring the `Register.DatabaseSessionAttributes` type. + +```ts +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseSessionAttributes: DatabaseSessionAttributes; + } + interface DatabaseSessionAttributes { + ip_country: string; + } +} +``` + +You can then include them in the session object with the `getSessionAttributes()` configuration. + +```ts +const lucia = new Lucia(adapter, { + getSessionAttributes: (attributes) => { + return { + ipCountry: attributes.ip_country + }; + } +}); + +const session = await lucia.createSession(); +session.ipCountry; +``` + +We do not automatically expose all database columns as + +1. Each project has its own code styling rules +2. You generally don't want to expose sensitive data (even worse if you send the entire session object to the client) + +## Create sessions + +You can create a new session with `Lucia.createSession()`, which takes a user ID and an empty object. + +```ts +const session = await lucia.createSession(userId, {}); +``` + +If you have database attributes defined, pass their values as the second argument. + +```ts +const session = await lucia.createSession(userId, { + country: "us" +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseSessionAttributes: DatabaseSessionAttributes; + } +} + +interface DatabaseSessionAttributes { + country: string; +} +``` + +## Validate sessions + +Use `Lucia.validateSession()` to validate a session using its ID. This will return an object containing a session and user. Both of these will be `null` if the session is invalid. + +```ts +const { session, user } = await lucia.validateSession(sessionId); +``` + +If `Session.fresh` is `true`, it indicates the session expiration has been extended and you should set a new session cookie. If you cannot always set a new session cookie due to limitations of your framework, set the [`sessionCookie.expires`](/basics/configuration#sessioncookie) option to `false`. + +```ts +const { session } = await lucia.validateSession(sessionId); +if (session && session.fresh) { + // set session cookie +} +``` + +You can use [`Lucia.readSessionCookie()`](/reference/main/Lucia/readSessionCookie) and [`Lucia.readBearerToken()`](/reference/main/Lucia/readBearerToken) to get the session ID from the `Cookie` and `Authorization` header respectively. + +```ts +const sessionId = lucia.readSessionCookie("auth_session=abc"); +const sessionId = lucia.readBearerToken("Bearer abc"); +``` + +See the [Validate session cookies](/guides/validate-session-cookies) and [Validate bearer tokens](/guides/validate-bearer-tokens) guide for a full example of validating session cookies. + +## Session cookies + +### Create session cookies + +You can create a session cookie for a session with [`Lucia.createSessionCookie()`](/reference/main/Lucia/createSessionCookie). It takes a session and returns a new [`Cookie`](/reference/main/Cookie) instance. You can either use [`Cookie.serialize()`](https://oslo.js.org/reference/cookie/Cookie/serialize) to create `Set-Cookie` HTTP header value, or use your framework's API by accessing the name, value, and session. + +```ts +const sessionCookie = lucia.createSessionCookie(session.id); + +// set cookie directly +headers.set("Set-Cookie", sessionCookie.serialize()); +// use your framework's cookie utility +setCookie(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); +``` + +### Delete session cookie + +You can delete a session cookie by setting a blank cookie created using [`Lucia.createBlankSessionCookie()`](/reference/main/Lucia/createBlankSessionCookie). + +```ts +const sessionCookie = lucia.createBlankSessionCookie(); + +headers.set("Set-Cookie", sessionCookie.serialize()); +setCookie(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); +``` + +## Invalidate sessions + +Use `Lucia.invalidateSession()` to invalidate a session. This should be used to sign out users. This will succeed even if the session ID is invalid. + +```ts +await lucia.invalidateSession(sessionId); +``` + +### Invalidate all user sessions + +Use `Lucia.invalidateUserSessions()` to invalidate all sessions belonging to a user. + +```ts +await lucia.invalidateUserSessions(userId); +``` + +## Get all user sessions + +Use `Lucia.getUserSessions()` to get all sessions belonging to a user. This will return an empty array if the user does not exist. Invalid sessions will be omitted from the array. + +```ts +const sessions = await lucia.getUserSessions(userId); +``` + +## Delete all expired sessions + +Use `Lucia.deleteExpiredSessions()` to delete all expired sessions in the database. We recommend setting up a cron-job to clean up your database on a set interval. + +```ts +await lucia.deleteExpiredSessions(); +``` diff --git a/docs/pages/translations/ja/basics/users.md b/docs/pages/translations/ja/basics/users.md new file mode 100644 index 000000000..68c7cd0b5 --- /dev/null +++ b/docs/pages/translations/ja/basics/users.md @@ -0,0 +1,74 @@ +--- +title: "Users" +--- + +# Users + +While Lucia does not provide APIs for creating and managing users, it still interacts with the user table. + +```ts +interface Session extends UserAttributes { + id: string; +} +``` + +## Create users + +When creating users, you can use `generateId()` to generate user IDs, which takes the length of the output string. This will generate a cryptographically secure random string consisting of lowercase letters and numbers. + +```ts +import { generateId } from "lucia"; + +await db.createUser({ + id: generateId(15) +}); +``` + +Use Oslo's [`generateRandomString()`](https://oslo.js.org/reference/crypto/generateRandomString) if you're looking for a more customizable option. + +```ts +import { generateRandomString, alphabet } from "oslo/crypto"; + +await db.createUser({ + id: generateRandomString(15, alphabet("a-z", "A-Z", "0-9")) +}); +``` + +## Define user attributes + +Defining custom session attributes requires 2 steps. First, add the required columns to the user table. You can type it by declaring the `Register.DatabaseUserAttributes` type (must be an interface). + +```ts +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: DatabaseUserAttributes; + } +} + +interface DatabaseUserAttributes { + username: string; +} +``` + +You can then include them in the user object with the `getUserAttributes()` configuration. + +```ts +const lucia = new Lucia(adapter, { + getUserAttributes: (attributes) => { + return { + username: attributes.username + }; + } +}); + +const { user } = await lucia.validateSession(); +if (user) { + const username = user.username; +} +``` + +We do not automatically expose all database columns as + +1. Each project has its own code styling rules +2. You generally don't want to expose sensitive data such as hashed passwords (even worse if you send the entire user object to the client) diff --git a/docs/pages/translations/ja/database/drizzle.md b/docs/pages/translations/ja/database/drizzle.md new file mode 100644 index 000000000..9d2eeb404 --- /dev/null +++ b/docs/pages/translations/ja/database/drizzle.md @@ -0,0 +1,107 @@ +--- +title: "Drizzle ORM" +--- + +# Drizzle ORM + +Adapters for Drizzle ORM are provided by `@lucia-auth/adapter-drizzle`. Supports MySQL, PostgreSQL, and SQLite. You're free to rename the underlying table and column names as long as the field names are the same (e.g. `expiresAt`). + +``` +npm install @lucia-auth/adapter-drizzle +``` + +## MySQL + +`DrizzleMySQLAdapter` takes a `Database` instance, the session table, and the user table. You can change the `varchar` length. `session(id)` should be able to hold at least 40 chars. + +```ts +import { DrizzleMySQLAdapter } from "@lucia-auth/adapter-drizzle"; + +import mysql from "mysql2/promise"; +import { mysqlTable, varchar, datetime } from "drizzle-orm/mysql-core"; +import { drizzle } from "drizzle-orm/mysql2"; + +const connection = await mysql.createConnection(); +const db = drizzle(connection); + +const userTable = mysqlTable("user", { + id: varchar("id", { + length: 255 + }).primaryKey() +}); + +const sessionTable = mysqlTable("session", { + id: varchar("id", { + length: 255 + }).primaryKey(), + userId: varchar("user_id", { + length: 255 + }) + .notNull() + .references(() => userTable.id), + expiresAt: datetime("expires_at").notNull() +}); + +const adapter = new DrizzleMySQLAdapter(db, sessionTable, userTable); +``` + +## PostgreSQL + +`DrizzlePostgreSQLAdapter` takes a `Database` instance, the session table, and the user table. + +```ts +import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle"; + +import pg from "pg"; +import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; +import { drizzle } from "drizzle-orm/node-postgres"; + +const pool = new pg.Pool(); +const db = drizzle(pool); + +const userTable = pgTable("user", { + id: text("id").primaryKey() +}); + +const sessionTable = pgTable("session", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => userTable.id), + expiresAt: timestamp("expires_at", { + withTimezone: true, + mode: "date" + }).notNull() +}); + +const adapter = new DrizzlePostgreSQLAdapter(db, sessionTable, userTable); +``` + +## SQLite + +`DrizzleSQLiteAdapter` takes a `Database` instance, the session table, and the user table. + +```ts +import { DrizzleSQLiteAdapter } from "@lucia-auth/adapter-drizzle"; + +import sqlite from "better-sqlite3"; +import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; +import { drizzle } from "drizzle-orm/better-sqlite3"; + +const sqliteDB = sqlite(":memory:"); +const db = drizzle(sqliteDB); + +const userTable = sqliteTable("user", { + id: text("id").notNull().primaryKey() +}); + +const sessionTable = sqliteTable("session", { + id: text("id").notNull().primaryKey(), + userId: text("user_id") + .notNull() + .references(() => userTable.id), + expiresAt: integer("expires_at").notNull() +}); + +const adapter = new DrizzleSQLiteAdapter(db, sessionTable, userTable); +``` diff --git a/docs/pages/translations/ja/database/index.md b/docs/pages/translations/ja/database/index.md new file mode 100644 index 000000000..0668636d2 --- /dev/null +++ b/docs/pages/translations/ja/database/index.md @@ -0,0 +1,29 @@ +--- +title: "Database" +--- + +# Database + +A database is required for storing your users and sessions. Lucia connects to your database via an adapter, which provides a set of basic, standardized querying methods that Lucia can use. + +```ts +import { Lucia } from "lucia"; +import { BetterSqlite3Adapter } from "@lucia-auth/adapter-sqlite"; + +const lucia = new Lucia(new BetterSqlite3Adapter(db)); +``` + +See [`Adapter`](/reference/main/Adapter) for building your own adapters. + +## Database setup + +Refer to these guides on setting up your database, ORMs, and query builders: + +- [Drizzle ORM](/database/drizzle) +- [Kysely](/database/kysely) +- [MongoDB](/database/mongodb) +- [Mongoose](/database/mongoose) +- [MySQL](/database/mysql): `mysql2`, PlanetScale serverless +- [PostgreSQL](/database/postgresql): node-postgres (`pg`), Postgres.js (`postgres`) +- [Prisma](/database/prisma) +- [SQLite](/database/sqlite): `better-sqlite3`, Bun SQLite (`bun:sqlite`), Cloudflare D1, LibSQL (Turso) diff --git a/docs/pages/translations/ja/database/kysely.md b/docs/pages/translations/ja/database/kysely.md new file mode 100644 index 000000000..8d56760bf --- /dev/null +++ b/docs/pages/translations/ja/database/kysely.md @@ -0,0 +1,118 @@ +--- +title: "Kysely" +--- + +# Kysely + +Lucia doesn't provide an adapter for Kysely but does provide adapters for drivers supported by Kysely. + +## MySQL + +See the [MySQL](/database/mysql) page for the schema. + +```ts +import { Lucia } from "lucia"; +import { Mysql2Adapter } from "@lucia-auth/adapter-mysql"; + +import { createPool } from "mysql2/promise"; +import { Kysely, MysqlDialect } from "kysely"; + +const pool = createPool(); + +const db = new Kysely({ + dialect: new MysqlDialect({ + pool: pool.pool // IMPORTANT NOT TO JUST PASS `pool` + }) +}); + +const adapter = new Mysql2Adapter(pool, tableNames); + +interface Database { + user: UserTable; + session: SessionTable; +} + +interface UserTable { + id: string; +} + +interface SessionTable { + id: string; + user_id: string; + expires_at: Date; +} +``` + +## PostgreSQL + +See the [PostgreSQL](/database/postgresql) page for the schema. + +```ts +import { Lucia } from "lucia"; +import { NodePostgresAdapter } from "@lucia-auth/adapter-postgresql"; + +import { Pool } from "pg"; +import { Kysely, PostgresDialect } from "kysely"; + +const pool = new Pool(); + +const db = new Kysely({ + dialect: new PostgresDialect({ + pool + }) +}); + +const adapter = new NodePostgresAdapter(pool, tableNames); + +interface Database { + user: UserTable; + session: SessionTable; +} + +interface UserTable { + id: string; +} + +interface SessionTable { + id: string; + user_id: string; + expires_at: Date; +} +``` + +## SQLite + +See the [SQLite](/database/sqlite) page for the schema. + +```ts +import { Lucia } from "lucia"; +import { BetterSqlite3Adapter } from "@lucia-auth/adapter-sqlite"; + +import sqlite from "better-sqlite3"; +import { Kysely, SqliteDialect } from "kysely"; + +const sqliteDatabase = sqlite(); + +export const db = new Kysely({ + dialect: new SqliteDialect({ + database: sqliteDatabase + }) +}); + +const adapter = new BetterSqlite3Adapter(sqliteDatabase, tableNames); + +interface Database { + user: UserTable; + session: SessionTable; +} + +interface UserTable { + id: string; +} + +interface SessionTable { + id: string; + user_id: string; + expires_at: number; +} +``` diff --git a/docs/pages/translations/ja/database/mongodb.md b/docs/pages/translations/ja/database/mongodb.md new file mode 100644 index 000000000..33fc94dd6 --- /dev/null +++ b/docs/pages/translations/ja/database/mongodb.md @@ -0,0 +1,40 @@ +--- +title: "MongoDB" +--- + +# MongoDB + +The `@lucia-auth/adapter-mongodb` package provides adapters for MongoDB. + +``` +npm install @lucia-auth/adapter-mongodb +``` + +## Usage + +You must handle the database connection manually. + +```ts +import { Lucia } from "lucia"; +import { MongoDBAdapter } from "@lucia-auth/adapter-mongodb"; +import { Collection, MongoClient } from "mongodb"; + +const client = new MongoClient(); +await client.connect(); + +const db = client.db(); +const User = db.collection("users") as Collection; +const Session = db.collection("sessions") as Collection; + +const adapter = new MongodbAdapter(Session, User); + +interface UserDoc { + _id: string; +} + +interface Session { + _id: string; + expires_at: Date; + user_id: string; +} +``` diff --git a/docs/pages/translations/ja/database/mongoose.md b/docs/pages/translations/ja/database/mongoose.md new file mode 100644 index 000000000..389b5cc2d --- /dev/null +++ b/docs/pages/translations/ja/database/mongoose.md @@ -0,0 +1,62 @@ +--- +title: "Mongoose" +--- + +# Mongoose + +You can use the [MongoDB adapter](/database/mongodb) from the `@lucia-auth/adapter-mongodb` package with Mongoose. + +``` +npm install @lucia-auth/adapter-mongodb +``` + +## Usage + +You must handle the database connection manually. + +```ts +import { Lucia } from "lucia"; +import { MongoDBAdapter } from "@lucia-auth/adapter-mongodb"; +import mongoose from "mongoose"; + +await mongoose.connect(); + +const User = mongoose.model( + "User", + new mongoose.Schema( + { + _id: { + type: String, + required: true + } + } as const, + { _id: false } + ) +); + +const Session = mongoose.model( + "Session", + new mongoose.Schema( + { + _id: { + type: String, + required: true + }, + user_id: { + type: String, + required: true + }, + expires_at: { + type: Date, + required: true + } + } as const, + { _id: false } + ) +); + +const adapter = new MongodbAdapter( + mongoose.connection.collection("sessions"), + mongoose.connection.collection("users") +); +``` diff --git a/docs/pages/translations/ja/database/mysql.md b/docs/pages/translations/ja/database/mysql.md new file mode 100644 index 000000000..10162126a --- /dev/null +++ b/docs/pages/translations/ja/database/mysql.md @@ -0,0 +1,66 @@ +--- +title: "MySQL" +--- + +# MySQL + +`@lucia-auth/adapter-mysql` package provides adapters for MySQL drivers: + +- `mysql2` +- PlanetScale serverless + +``` +npm install @lucia-auth/adapter-mysql +``` + +## Schema + +You can change the `varchar` length as necessary. `session(id)` should be able to hold at least 40 chars. + +```sql +CREATE TABLE user ( + id VARCHAR(255) PRIMARY KEY +) + +CREATE TABLE user_session ( + id VARCHAR(255) PRIMARY KEY, + expires_at DATETIME NOT NULL, + user_id VARCHAR(255) NOT NULL REFERENCES user(id) +) +``` + +## Drivers + +### `mysql2` + +`Mysql2Adapter` takes a `Pool` or `Connection` instance from `mysql2/promises` and a list of table names. + +```ts +import { Lucia } from "lucia"; +import { Mysql2Adapter } from "@lucia-auth/adapter-mysql"; +import mysql from "mysql2/promise"; + +const pool = mysql.createPool(); + +const adapter = new Mysql2Adapter(pool, { + user: "user", + session: "user_session" +}); +``` + +### PlanetScale serverless + +`PlanetScaleAdapter` takes a `Connection` instance and a list of table names. + +```ts +import { Lucia } from "lucia"; +import { PlanetScaleAdapter } from "@lucia-auth/adapter-mysql"; +import { connect } from "@planetscale/database"; + +const connection = connect(); + +const adapter = new PlanetScaleAdapter(connection, { + user: "user", + session: "user_session" +}); +``` diff --git a/docs/pages/translations/ja/database/postgresql.md b/docs/pages/translations/ja/database/postgresql.md new file mode 100644 index 000000000..839751476 --- /dev/null +++ b/docs/pages/translations/ja/database/postgresql.md @@ -0,0 +1,64 @@ +--- +title: "PostgreSQL" +--- + +# PostgreSQL + +`@lucia-auth/adapter-postgresql` package provides adapters for PostgreSQL drivers: + +- node-postgres (`pg`) +- Postgres.js (`postgres`) + +``` +npm install @lucia-auth/adapter-postgresql +``` + +## Schema + +```sql +CREATE TABLE auth_user ( + id TEXT PRIMARY KEY +) + +CREATE TABLE user_session ( + id TEXT PRIMARY KEY, + expires_at TIMESTAMPTZ NOT NULL, + user_id TEXT NOT NULL REFERENCES auth_user(id) +) +``` + +## Drivers + +### node-postgres + +`NodePostgresAdapter` takes a `Pool` or `Client` instance and a list of table names. + +```ts +import { Lucia } from "lucia"; +import { NodePostgresAdapter } from "@lucia-auth/adapter-postgresql"; +import pg from "pg"; + +const pool = new pg.Pool(); + +const adapter = new NodePostgresAdapter(pool, { + user: "auth_user", + session: "user_session" +}); +``` + +### Postgres.js + +`PostgresJsAdapter` takes a `Sql` instance and a list of table names. + +```ts +import { Lucia } from "lucia"; +import { PostgresJsAdapter } from "@lucia-auth/adapter-postgresql"; +import postgres from "postgres"; + +const sql = postgres(); + +const adapter = new PostgresJsAdapter(sql, { + user: "auth_user", + session: "user_session" +}); +``` diff --git a/docs/pages/translations/ja/database/prisma.md b/docs/pages/translations/ja/database/prisma.md new file mode 100644 index 000000000..e80516192 --- /dev/null +++ b/docs/pages/translations/ja/database/prisma.md @@ -0,0 +1,41 @@ +--- +title: "Prisma" +--- + +# Prisma + +The `@lucia-auth/adapter-prisma` package provides adapters for Prisma. + +``` +npm install @lucia-auth/adapter-prisma +``` + +## Schema + +```prisma +model User { + id String @id + sessions Session[] +} + +model Session { + id String @id + userId String + expiresAt DateTime + user User @relation(references: [id], fields: [userId], onDelete: Cascade) +} +``` + +## Usage + +`PrismaAdapter` takes a session and user model. + +```ts +import { Lucia } from "lucia"; +import { PrismaAdapter } from "@lucia-auth/adapter-prisma"; +import { PrismaClient } from "@prisma/client"; + +const client = new PrismaClient(); + +const adapter = new PrismaAdapter(client.session, client.user); +``` diff --git a/docs/pages/translations/ja/database/sqlite.md b/docs/pages/translations/ja/database/sqlite.md new file mode 100644 index 000000000..eb0a507e5 --- /dev/null +++ b/docs/pages/translations/ja/database/sqlite.md @@ -0,0 +1,111 @@ +--- +title: "SQLite" +--- + +# SQLite + +The `@lucia-auth/adapter-sqlite` package provides adapters for SQLites drivers: + +- `better-sqlite3` +- Bun SQLite (`bun:sqlite`) +- Cloudflare D1 +- LibSQL (Turso) + +``` +npm install @lucia-auth/adapter-sqlite +``` + +## Schema + +```sql +CREATE TABLE user ( + id TEXT NOT NULL PRIMARY KEY +) + +CREATE TABLE session ( + id TEXT NOT NULL PRIMARY KEY, + expires_at INTEGER NOT NULL, + user_id TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES user(id) +) +``` + +## Drivers + +### `better-sqlite3` + +`BetterSqlite3Adapter` takes a `Database` instance and a list of table names. + +```ts +import { Lucia } from "lucia"; +import { BetterSqlite3Adapter } from "@lucia-auth/adapter-sqlite"; +import sqlite from "better-sqlite3"; + +const db = sqlite(); + +const adapter = new BetterSqlite3Adapter(db, { + user: "user", + session: "session" +}); +``` + +### Bun SQLite + +`BunSQLiteAdapter` takes a `Database` instance and a list of table names. + +```ts +import { Lucia } from "lucia"; +import { BunSQLiteAdapter } from "@lucia-auth/adapter-sqlite"; +import { Database } from "bun:sqlite"; + +const db = new Database(); + +const adapter = new BunSQLiteAdapter(db, { + user: "user", + session: "session" +}); +``` + +### Cloudflare D1 + +`D1Adapter` takes a `D1Database` instance and a list of table names. + +Since the D1 binding is included with the request, create an `initializeLucia()` function to create a new `Lucia` instance on every request. + +```ts +import { Lucia } from "lucia"; +import { D1Adapter } from "@lucia-auth/adapter-sqlite"; + +export function initializeLucia(D1: D1Database) { + const adapter = new D1Adapter(D1, { + user: "user", + session: "session" + }); + return new Lucia(adapter); +} + +declare module "lucia" { + interface Register { + Auth: ReturnType; + } +} +``` + +### LibSQL + +`LibSQLAdapter` takes a `D1Database` instance and a list of table names. + +```ts +import { Lucia } from "lucia"; +import { LibSQLAdapter } from "@lucia-auth/adapter-sqlite"; +import { createClient } from "@libsql/client"; + +const db = createClient({ + url: "file:test/main.db" +}); + +const adapter = new LibSQLAdapter(db, { + user: "user", + session: "session" +}); +``` diff --git a/docs/pages/translations/ja/getting-started/astro.md b/docs/pages/translations/ja/getting-started/astro.md new file mode 100644 index 000000000..5a05e9d67 --- /dev/null +++ b/docs/pages/translations/ja/getting-started/astro.md @@ -0,0 +1,120 @@ +--- +title: "Getting started in Astro" +--- + +# Getting started in Astro + +## Installation + +Install Lucia using your package manager of your choice. While not strictly necessary, we recommend installing [Oslo](https://oslo.js.org), which Lucia is built on, for various auth utilities (which a lot of the guides use). + +``` +npm install lucia oslo +``` + +## Update Vite + +If you've installed `oslo`, add `oslo` to `vite.optimizeDeps.exclude`. + +```ts +// astro.config.mjs +export default defineConfig({ + // ... + vite: { + optimizeDeps: { + exclude: ["oslo"] + } + } +}); +``` + +## Initialize Lucia + +Import `Lucia` and initialize it with your adapter. Refer to the [Database](/database) page to learn how to set up your database and initialize the adapter. Make sure to configure the `sessionCookie` option and register your `Lucia` instance type + +```ts +// src/auth.ts +import { Lucia } from "lucia"; + +const adapter = new BetterSQLite3Adapter(db); // your adapter + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + // set to `true` when using HTTPS + secure: import.meta.env.PROD + } + } +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + } +} +``` + +## Set up middleware + +We recommend setting up a middleware to validate requests. The validated user will be available as `local.user`. You can just copy-paste the code into `src/middleware.ts`. + +It's a bit verbose, but it just reads the session cookie, validates it, and sets a new cookie if necessary. Since Astro doesn't implement CSRF protection out of the box, it must be implemented. If you're curious about what's happening here, see the [Validating requests](/basics/validate-session-cookies/astro) page. + +```ts +// src/middleware.ts +import { lucia } from "./auth"; +import { verifyRequestOrigin } from "lucia"; +import { defineMiddleware } from "astro:middleware"; + +export const onRequest = defineMiddleware(async (context, next) => { + if (context.request.method !== "GET") { + const originHeader = context.request.headers.get("Origin"); + const hostHeader = context.request.headers.get("Host"); + if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { + return new Response(null, { + status: 403 + }); + } + } + + const sessionId = context.cookies.get(lucia.sessionCookieName)?.value ?? null; + if (!sessionId) { + context.locals.user = null; + context.locals.session = null; + return next(); + } + + const { session, user } = await lucia.validateSession(sessionId); + if (session && session.fresh) { + const sessionCookie = lucia.createSessionCookie(session.id); + context.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + } + if (!session) { + const sessionCookie = lucia.createBlankSessionCookie(); + context.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + } + context.locals.session = session; + context.locals.user = user; + return next(); +}); +``` + +Make sure sure to type `App.Locals` as well. + +```ts +// src/env.d.ts + +/// +declare namespace App { + interface Locals { + session: import("lucia").Session | null; + user: import("lucia").User | null; + } +} +``` + +## Next steps + +You can learn all the concepts and APIs by reading the [Basics section](/basics/sessions) in the docs. If you prefer writing code immediately, check out the [Tutorials](/tutorials) page or the [examples repository](https://github.com/lucia-auth/examples/tree/main). + +If you have any questions, [join our Discord server](https://discord.com/invite/PwrK3kpVR3)! diff --git a/docs/pages/translations/ja/getting-started/express.md b/docs/pages/translations/ja/getting-started/express.md new file mode 100644 index 000000000..512d8f283 --- /dev/null +++ b/docs/pages/translations/ja/getting-started/express.md @@ -0,0 +1,59 @@ +--- +title: "Getting started in Express" +--- + +# Getting started in Express + +## Installation + +Install Lucia using your package manager of your choice. While not strictly necessary, we recommend installing [Oslo](https://oslo.js.org), which Lucia is built on, for various auth utilities (which a lot of the guides use). + +``` +npm install lucia oslo +``` + +## Initialize Lucia + +Import `Lucia` and initialize it with your adapter. Refer to the [Database](/database) page to learn how to set up your database and initialize the adapter. Make sure you configure the `sessionCookie` option and register your `Lucia` instance type. + +```ts +import { Lucia } from "lucia"; + +const adapter = new BetterSQLite3Adapter(db); // your adapter + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + // set to `true` when using HTTPS + secure: process.env.NODE_ENV === "production" + } + } +}); + +// IMPORTANT! +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + } +} +``` + +## Polyfill + +If you're using Node.js 18 or below, you'll need to polyfill the Web Crypto API. This is not required in Node.js 20, CloudFlare Workers, Deno, Bun, and Vercel Edge Functions. This can be done either by importing `webcrypto`, or by enabling an experimental flag. + +```ts +import { webcrypto } from "node:crypto"; + +globalThis.crypto = webcrypto as Crypto; +``` + +``` +node --experimental-web-crypto index.js +``` + +## Next steps + +You can learn all the concepts and APIs by reading the [Basics section](/basics/sessions) in the docs. If you prefer writing code immediately, check out the [Tutorials](/tutorials) page or the [examples repository](https://github.com/lucia-auth/examples/tree/main). + +If you have any questions, [join our Discord server](https://discord.com/invite/PwrK3kpVR3)! diff --git a/docs/pages/translations/ja/getting-started/index.md b/docs/pages/translations/ja/getting-started/index.md new file mode 100644 index 000000000..cb57c38a4 --- /dev/null +++ b/docs/pages/translations/ja/getting-started/index.md @@ -0,0 +1,91 @@ +--- +title: "Getting started" +--- + +# Getting started + +A framework-specific guide is also available for: + +- [Astro](/getting-started/astro) +- [Express](/getting-started/express) +- [Next.js App router](/getting-started/nextjs-app) +- [Next.js Pages router](/getting-started/nextjs-pages) +- [Nuxt](/getting-started/nuxt) +- [SolidStart](/getting-started/solidstart) +- [SvelteKit](/getting-started/sveltekit) + +## Installation + +Install Lucia using your package manager of your choice. While not strictly necessary, we recommend installing [Oslo](https://oslo.js.org), which Lucia is built on, for various auth utilities (which a lot of the guides use). + +``` +npm install lucia oslo +``` + +## Initialize Lucia + +Import `Lucia` and initialize it with your adapter. Refer to the [Database](/database) page to learn how to set up your database and initialize the adapter. Make sure you configure the `sessionCookie` option and register your `Lucia` instance type. + +```ts +import { Lucia } from "lucia"; + +const adapter = new BetterSQLite3Adapter(db); // your adapter + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + // set to `true` when using HTTPS + secure: process.env.NODE_ENV === "production" + } + } +}); + +// IMPORTANT! +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + } +} +``` + +## Polyfill + +If you're using Node.js 18 or below, you'll need to polyfill the Web Crypto API. This is not required in Node.js 20, CloudFlare Workers, Deno, Bun, and Vercel Edge Functions. This can be done either by importing `webcrypto`, or by enabling an experimental flag. + +```ts +import { webcrypto } from "node:crypto"; + +globalThis.crypto = webcrypto as Crypto; +``` + +``` +node --experimental-web-crypto index.js +``` + +## Update bundler configuration + +This is only required if you're using `oslo/password`. + +### Vite + +This is not required if you're Nuxt, SolidStart, or SvelteKit. + +```ts +import { defineConfig } from "vite"; + +export default defineConfig({ + // ... + optimizeDeps: { + exclude: ["oslo"] + } +}); +``` + +### Webpack + +```ts +module.exports = { + // ... + externals: ["@node-rs/argon2", "@node-rs/bcrypt"] +}; +``` diff --git a/docs/pages/translations/ja/getting-started/nextjs-app.md b/docs/pages/translations/ja/getting-started/nextjs-app.md new file mode 100644 index 000000000..b1676ca9a --- /dev/null +++ b/docs/pages/translations/ja/getting-started/nextjs-app.md @@ -0,0 +1,79 @@ +--- +title: "Getting started in Next.js App router" +--- + +# Getting started in Next.js App router + +## Installation + +Install Lucia using your package manager of your choice. While not strictly necessary, we recommend installing [Oslo](https://oslo.js.org), which Lucia is built on, for various auth utilities (which a lot of the guides in the docs use). + +``` +npm install lucia oslo +``` + +**`oslo/password` does NOT work with Turbopack.** + +## Initialize Lucia + +Import `Lucia` and initialize it with your adapter. Refer to the [Database](/database) page to learn how to set up your database and initialize the adapter. Make sure you configure the `sessionCookie` option and register your `Lucia` instance type. + +```ts +// src/auth.ts +import { Lucia } from "lucia"; + +const adapter = new BetterSQLite3Adapter(db); // your adapter + +export const lucia = new Lucia(adapter, { + sessionCookie: { + // this sets cookies with super long expiration + // since Next.js doesn't allow Lucia to extend cookie expiration when rendering pages + expires: false, + attributes: { + // set to `true` when using HTTPS + secure: process.env.NODE_ENV === "production" + } + } +}); + +// IMPORTANT! +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + } +} +``` + +## Polyfill + +If you're using Node.js 18 or below, you'll need to polyfill the Web Crypto API. This is not required in Node.js 20, CloudFlare Workers, Deno, Bun, and Vercel Edge Functions. This can be done either by importing `webcrypto`, or by enabling an experimental flag. + +```ts +import { webcrypto } from "node:crypto"; + +globalThis.crypto = webcrypto as Crypto; +``` + +``` +node --experimental-web-crypto index.js +``` + +## Update configuration + +If you've installed Oslo, mark its dependencies as external to prevent it from getting bundled. This is only required when using the `oslo/password` module. + +```ts +// next.config.ts +const nextConfig = { + webpack: (config) => { + config.externals.push("@node-rs/argon2", "@node-rs/bcrypt"); + return config; + } +}; +``` + +## Next steps + +You can learn all the concepts and APIs by reading the [Basics section](/basics/sessions) in the docs. If you prefer writing code immediately, check out the [Tutorials](/tutorials) page or the [examples repository](https://github.com/lucia-auth/examples/tree/main). + +If you have any questions, [join our Discord server](https://discord.com/invite/PwrK3kpVR3)! diff --git a/docs/pages/translations/ja/getting-started/nextjs-pages.md b/docs/pages/translations/ja/getting-started/nextjs-pages.md new file mode 100644 index 000000000..691d22880 --- /dev/null +++ b/docs/pages/translations/ja/getting-started/nextjs-pages.md @@ -0,0 +1,85 @@ +--- +title: "Getting started in Next.js Pages router" +--- + +# Getting started in Next.js Pages router + +## Installation + +Install Lucia using your package manager of your choice. While not strictly necessary, we recommend installing [Oslo](https://oslo.js.org), which Lucia is built on, for various auth utilities (which a lot of the guides use). + +``` +npm install lucia oslo +``` + +## Initialize Lucia + +Import `Lucia` and initialize it with your adapter. Refer to the [Database](/database) page to learn how to set up your database and initialize the adapter. Make sure you configure the `sessionCookie` option and register your `Lucia` instance type. + +```ts +// src/auth.ts +import { Lucia } from "lucia"; + +const adapter = new BetterSQLite3Adapter(db); // your adapter + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + // set to `true` when using HTTPS + secure: process.env.NODE_ENV === "production" + } + } +}); + +// IMPORTANT! +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + } +} +``` + +## Polyfill + +If you're using Node.js 18 or below, you'll need to polyfill the Web Crypto API. This is not required in Node.js 20, CloudFlare Workers, Deno, Bun, and Vercel Edge Functions. This can be done either by importing `webcrypto`, or by enabling an experimental flag. + +```ts +import { webcrypto } from "node:crypto"; + +globalThis.crypto = webcrypto as Crypto; +``` + +``` +node --experimental-web-crypto index.js +``` + +## Set up middleware + +If you're planning to use cookies, you must implement CSRF protection. + +```ts +// middleware.ts +import { verifyRequestOrigin } from "lucia"; +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +export async function middleware(request: NextRequest): Promise { + if (request.method === "GET") { + return NextResponse.next(); + } + const originHeader = request.headers.get("Origin"); + const hostHeader = request.headers.get("Host"); + if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { + return new NextResponse(null, { + status: 403 + }); + } + return NextResponse.next(); +} +``` + +## Next steps + +You can learn all the concepts and APIs by reading the [Basics section](/basics/sessions) in the docs. If you prefer writing code immediately, check out the [Tutorials](/tutorials) page or the [examples repository](https://github.com/lucia-auth/examples/tree/main). + +If you have any questions, [join our Discord server](https://discord.com/invite/PwrK3kpVR3)! diff --git a/docs/pages/translations/ja/getting-started/nuxt.md b/docs/pages/translations/ja/getting-started/nuxt.md new file mode 100644 index 000000000..c2c00c531 --- /dev/null +++ b/docs/pages/translations/ja/getting-started/nuxt.md @@ -0,0 +1,111 @@ +--- +title: "Getting started in Nuxt" +--- + +# Getting started in Nuxt + +## Installation + +Install Lucia using your package manager of your choice. While not strictly necessary, we recommend installing [Oslo](https://oslo.js.org), which Lucia is built on, for various auth utilities (which a lot of the guides use). + +``` +npm install lucia oslo +``` + +## Initialize Lucia + +Import `Lucia` and initialize it with your adapter. Refer to the [Database](/database) page to learn how to set up your database and initialize the adapter. Make sure you configure the `sessionCookie` option and register your `Lucia` instance type. + +- Configure the `sessionCookie` option +- Register your `Lucia` instance type + +```ts +// server/utils/auth.ts +import { Lucia } from "lucia"; + +const adapter = new BetterSQLite3Adapter(db); // your adapter + +export const lucia = new Lucia(adapter, { + sessionCookie: { + // IMPORTANT! + attributes: { + // set to `true` when using HTTPS + secure: !process.dev + } + } +}); + +// IMPORTANT! +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + } +} +``` + +## Polyfill + +If you're using Node.js 18 or below, you'll need to polyfill the Web Crypto API. This is not required in Node.js 20, CloudFlare Workers, Deno, Bun, and Vercel Edge Functions. This can be done either by importing `webcrypto`, or by enabling an experimental flag. + +```ts +import { webcrypto } from "node:crypto"; + +globalThis.crypto = webcrypto as Crypto; +``` + +``` +node --experimental-web-crypto index.js +``` + +## Set up middleware + +We recommend setting up a middleware to validate requests. The validated user will be available as `event.context.user`. You can just copy-paste the code into `server/middleware/auth.ts`. + +It's a bit verbose, but it just reads the session cookie, validates it, and sets a new cookie if necessary. Since Nuxt doesn't implement CSRF protection out of the box, it must be implemented. If you're curious about what's happening here, see the [Validating requests](/basics/validate-session-cookies/nuxt) page. + +```ts +// server/middleware/auth.ts +import { verifyRequestOrigin } from "lucia"; + +import type { Session, User } from "lucia"; + +export default defineEventHandler(async (event) => { + if (event.method !== "GET") { + const originHeader = getHeader(event, "Origin") ?? null; + const hostHeader = getHeader(event, "Host") ?? null; + if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { + return event.node.res.writeHead(403).end(); + } + } + + const sessionId = getCookie(event, lucia.sessionCookieName) ?? null; + if (!sessionId) { + event.context.session = null; + event.context.user = null; + return; + } + + const { session, user } = await lucia.validateSession(sessionId); + if (session && session.fresh) { + appendResponseHeader(event, "Set-Cookie", lucia.createSessionCookie(session.id).serialize()); + } + if (!session) { + appendResponseHeader(event, "Set-Cookie", lucia.createBlankSessionCookie().serialize()); + } + event.context.session = session; + event.context.user = user; +}); + +declare module "h3" { + interface H3EventContext { + user: User | null; + session: Session | null; + } +} +``` + +## Next steps + +You can learn all the concepts and APIs by reading the [Basics section](/basics/sessions) in the docs. If you prefer writing code immediately, check out the [Tutorials](/tutorials) page or the [examples repository](https://github.com/lucia-auth/examples/tree/main). + +If you have any questions, [join our Discord server](https://discord.com/invite/PwrK3kpVR3)! diff --git a/docs/pages/translations/ja/getting-started/solidstart.md b/docs/pages/translations/ja/getting-started/solidstart.md new file mode 100644 index 000000000..c46846037 --- /dev/null +++ b/docs/pages/translations/ja/getting-started/solidstart.md @@ -0,0 +1,108 @@ +--- +title: "Getting started in SolidStart" +--- + +# Getting started in SolidStart + +## Installation + +Install Lucia using your package manager of your choice. While not strictly necessary, we recommend installing [Oslo](https://oslo.js.org), which Lucia is built on, for various auth utilities (which a lot of the guides use). + +``` +npm install lucia oslo +``` + +## Initialize Lucia + +Import `Lucia` and initialize it with your adapter. Refer to the [Database](/database) page to learn how to set up your database and initialize the adapter. Make sure to configure the `sessionCookie` option and register your `Lucia` instance type + +```ts +// src/lib/auth.ts +import { Lucia } from "lucia"; + +const adapter = new BetterSQLite3Adapter(db); // your adapter + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + // set to `true` when using HTTPS + secure: import.meta.env.PROD + } + } +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + } +} +``` + +## Set up middleware + +We recommend setting up a middleware to validate requests. The validated user will be available as `context.user`. You can just copy-paste the code into `src/middleware.ts`. + +It's a bit verbose, but it just reads the session cookie, validates it, and sets a new cookie if necessary. Since SolidStart doesn't implement CSRF protection out of the box, it must be implemented when working with cookies. If you're curious about what's happening here, see the [Validating requests](/basics/validate-session-cookies/solidstart) page. + +```ts +// src/middleware.ts +import { createMiddleware, appendHeader, getCookie, getHeader } from "@solidjs/start/server"; +import { Session, User, verifyRequestOrigin } from "lucia"; +import { lucia } from "./lib/auth"; + +export default createMiddleware({ + onRequest: async (event) => { + if (event.node.req.method !== "GET") { + const originHeader = getHeader(event, "Origin") ?? null; + const hostHeader = getHeader(event, "Host") ?? null; + if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { + event.node.res.writeHead(403).end(); + return; + } + } + + const sessionId = getCookie(event, lucia.sessionCookieName) ?? null; + if (!sessionId) { + event.context.session = null; + event.context.user = null; + return; + } + + const { session, user } = await lucia.validateSession(sessionId); + if (session && session.fresh) { + appendHeader(event, "Set-Cookie", lucia.createSessionCookie(session.id).serialize()); + } + if (!session) { + appendHeader(event, "Set-Cookie", lucia.createBlankSessionCookie().serialize()); + } + event.context.session = session; + event.context.user = user; + } +}); + +declare module "vinxi/server" { + interface H3EventContext { + user: User | null; + session: Session | null; + } +} +``` + +Make sure to declare the middleware module in the config. + +```ts +// vite.config.ts +import { defineConfig } from "@solidjs/start/config"; + +export default defineConfig({ + start: { + middleware: "./src/middleware.ts" + } +}); +``` + +## Next steps + +You can learn all the concepts and APIs by reading the [Basics section](/basics/sessions) in the docs. If you prefer writing code immediately, check out the [Tutorials](/tutorials) page or the [examples repository](https://github.com/lucia-auth/examples/tree/main). + +If you have any questions, [join our Discord server](https://discord.com/invite/PwrK3kpVR3)! diff --git a/docs/pages/translations/ja/getting-started/sveltekit.md b/docs/pages/translations/ja/getting-started/sveltekit.md new file mode 100644 index 000000000..8fef2540f --- /dev/null +++ b/docs/pages/translations/ja/getting-started/sveltekit.md @@ -0,0 +1,104 @@ +--- +title: "Getting started in Sveltekit" +--- + +# Getting started in Sveltekit + +## Installation + +Install Lucia using your package manager of your choice. While not strictly necessary, we recommend installing [Oslo](https://oslo.js.org), which Lucia is built on, for various auth utilities (which a lot of the guides use). + +``` +npm install lucia oslo +``` + +## Initialize Lucia + +Import `Lucia` and initialize it with your adapter. Refer to the [Database](/database) page to learn how to set up your database and initialize the adapter. Make sure to configure the `sessionCookie` option and register your `Lucia` instance type + +```ts +// src/lib/server/auth.ts +import { Lucia } from "lucia"; +import { dev } from "$app/environment"; + +const adapter = new BetterSQLite3Adapter(db); // your adapter + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + // set to `true` when using HTTPS + secure: !dev + } + } +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + } +} +``` + +## Setup hooks + +We recommend setting up a handle hook to validate requests. The validated user will be available as `local.user`. + +If you're curious about what's happening here, see the [Validating requests](/basics/validate-session-cookies/sveltekit) page. + +```ts +// src/hooks.server.ts +import { lucia } from "$lib/server/auth"; +import type { Handle } from "@sveltejs/kit"; + +export const handle: Handle = async ({ event, resolve }) => { + const sessionId = event.cookies.get(lucia.sessionCookieName); + if (!sessionId) { + event.locals.user = null; + event.locals.session = null; + return resolve(event); + } + + const { session, user } = await lucia.validateSession(sessionId); + if (session && session.fresh) { + const sessionCookie = lucia.createSessionCookie(session.id); + // sveltekit types deviates from the de-facto standard + // you can use 'as any' too + event.cookies.set(sessionCookie.name, sessionCookie.value, { + path: ".", + ...sessionCookie.attributes + }); + } + if (!session) { + const sessionCookie = lucia.createBlankSessionCookie(); + event.cookies.set(sessionCookie.name, sessionCookie.value, { + path: ".", + ...sessionCookie.attributes + }); + } + event.locals.user = user; + event.locals.session = session; + return resolve(event); +}; +``` + +Make sure sure to type `App.Locals` as well. + +```ts +// src/app.d.ts +declare global { + namespace App { + interface Locals { + user: import("lucia").User | null; + session: import("lucia").Session | null; + } + } +} + +export {}; +``` + +## Next steps + +You can learn all the concepts and APIs by reading the [Basics section](/basics/sessions) in the docs. If you prefer writing code immediately, check out the [Tutorials](/tutorials) page or the [examples repository](https://github.com/lucia-auth/examples/tree/main). + +If you have any questions, [join our Discord server](https://discord.com/invite/PwrK3kpVR3)! diff --git a/docs/pages/translations/ja/guides/email-and-password/2fa.md b/docs/pages/translations/ja/guides/email-and-password/2fa.md new file mode 100644 index 000000000..454266190 --- /dev/null +++ b/docs/pages/translations/ja/guides/email-and-password/2fa.md @@ -0,0 +1,109 @@ +--- +title: "Two-factor authorization" +--- + +# Two-factor authorization + +The guide covers how to implement two-factor authorization using time-based OTP (TOTP) and authenticator apps. + +## Update database + +Update the user table to include `two_factor_secret` column. You can of course store the secret in its own table. + +```ts +import { Lucia } from "lucia"; + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: env === "PRODUCTION" // set `Secure` flag in HTTPS + } + }, + getUserAttributes: (attributes) => { + return { + // ... + // don't expose the secret + // rather expose whether if the user has setup 2fa + setupTwoFactor: attributes.two_factor_secret !== null + }; + } +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: { + two_factor_secret: string | null; + }; + } +} +``` + +## Create QR code + +When the user signs up, set `two_factor_secret` to `null` to indicate the user has yet to set up two-factor authorization. + +```ts +app.post("/signup", async () => { + // ... + + const userId = generateId(); + + await db.table("user").insert({ + id: userId, + two_factor_secret: null + // ... + }); + + // ... +}); +``` + +Generate a new secret (minimum 20 bytes) and create a new key URI with [`createTOTPKeyURI()`](https://oslo.js.org/reference/otp/createTOTPKeyURI). The user should scan the QR code using their authenticator app. + +```ts +import { encodeHex } from "oslo/encoding"; +import { createTOTPKeyURI } from "oslo/otp"; + +const { user } = await lucia.validateSession(sessionId); +if (!user) { + return new Response(null, { + status: 401 + }); +} + +const twoFactorSecret = crypto.getRandomValues(new Uint8Array(20)); +await db + .table("user") + .where("id", "=", user.id) + .update({ + two_factor_secret: encodeHex(twoFactorSecret) + }); + +// pass the website's name and the user identifier (e.g. email, username) +const uri = createTOTPKeyURI("my-app", user.email, twoFactorSecret); + +// use any image generator +const qrcode = createQRCode(uri); +``` + +## Validate OTP + +Validate TOTP with [`TOTPController`](https://oslo.js.org/reference/otp/TOTPController) using the stored user's secret. + +```ts +import { decodeHex } from "oslo/encoding"; +import { TOTPController } from "oslo/otp"; + +let otp: string; + +const { user } = await lucia.validateSession(sessionId); +if (!user) { + return new Response(null, { + status: 401 + }); +} + +const result = await db.table("user").where("id", "=", user.id).get("two_factor_secret"); +const validOTP = await new TOTPController().verify(otp, decodeHex(result.two_factor_secret)); +``` diff --git a/docs/pages/translations/ja/guides/email-and-password/basics.md b/docs/pages/translations/ja/guides/email-and-password/basics.md new file mode 100644 index 000000000..9043da695 --- /dev/null +++ b/docs/pages/translations/ja/guides/email-and-password/basics.md @@ -0,0 +1,183 @@ +--- +title: "Password basics" +--- + +# Password basics + +This page covers how to implement a password-based auth with Lucia. If you're looking for a step-by-step, framework-specific tutorial, you may want to check out the [Username and password](/tutorials/username-and-password) tutorial. Keep in mind that email-based auth requires more than just passwords! + +## Update database + +Add a unique `email` and `hashed_password` column to the user table. + +| column | type | attributes | +| ----------------- | -------- | ---------- | +| `email` | `string` | unique | +| `hashed_password` | `string` | | + +Declare the type with `DatabaseUserAttributes` and add the attributes to the user object using the `getUserAttributes()` configuration. + +```ts +// auth.ts +import { Lucia } from "lucia"; + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: env === "PRODUCTION" // set `Secure` flag in HTTPS + } + }, + getUserAttributes: (attributes) => { + return { + // we don't need to expose the hashed password! + email: attributes.email + }; + } +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: { + email: string; + }; + } +} +``` + +## Email check + +Before creating routes, create a basic utility to verify emails. Emails are notoriously complicated, so here we're just checking if an `@` exists with at least 1 character on each side. We just need to check for obvious typos here. For verifying emails, see one of the email verification guides. + +```ts +export function isValidEmail(email: string): boolean { + return /.+@.+/.test(email); +} +``` + +## Register user + +Create a `/signup` route. This will accept POST requests with an email and password. Hash the password, create a new user, and create a new session. + +```ts +import { lucia } from "./auth.js"; +import { generateId } from "lucia"; +import { Argon2id } from "oslo/password"; + +app.post("/signup", async (request: Request) => { + const formData = await request.formData(); + const email = formData.get("email"); + if (!email || typeof email !== "string" || !isValidEmail(email)) { + return new Response("Invalid email", { + status: 400 + }); + } + const password = formData.get("password"); + if (!password || typeof password !== "string" || password.length < 6) { + return new Response("Invalid password", { + status: 400 + }); + } + + const hashedPassword = await new Argon2id().hash(password); + const userId = generateId(15); + + try { + await db.table("user").insert({ + id: userId, + email, + hashed_password: hashedPassword + }); + + const session = await lucia.createSession(userId, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + return new Response(null, { + status: 302, + headers: { + Location: "/", + "Set-Cookie": sessionCookie.serialize() + } + }); + } catch { + // db error, email taken, etc + return new Response("Email already used", { + status: 400 + }); + } +}); +``` + +### Hashing passwords + +`oslo/password` currently provides [`Argon2id`](https://oslo.js.org/reference/password/Argon2id), [`Scrypt`](https://oslo.js.org/reference/password/Scrypt), and [`Bcrypt`](https://oslo.js.org/reference/password/Bcrypt). These rely on the fastest available libraries but only work in Node.js. Passwords are salted and hashed using settings recommended by OWASP. + +```ts +import { Argon2id, Scrypt, Bcrypt } from "oslo/password"; +``` + +For Bun, we recommend using [`Bun.password`](https://bun.sh/docs/api/hashing), which also uses Argon2id by default. For other runtimes, Lucia provides a pure-JS implementation of Scrypt with [`Scrypt`](/reference/main/Scrypt) that works in any environment. However, we do not recommend this for Node.js as it can be 2~3 times slower than the Node-only version. If you're migrating from Lucia v2, you should use [`LegacyScrypt`](/reference/main/LegacyScrypt). + +```ts +import { Scrypt, LegacyScrypt } from "lucia"; +``` + +## Sign in user + +Create a `/login` route. This will accept POST requests with an email and password. Get the user with the email, verify the password against the hash, and create a new session. + +```ts +import { lucia } from "./auth.js"; +import { generateId } from "lucia"; +import { Argon2id } from "oslo/password"; + +app.post("/login", async (request: Request) => { + const formData = await request.formData(); + const email = formData.get("email"); + if (!email || typeof email !== "string") { + return new Response("Invalid email", { + status: 400 + }); + } + const password = formData.get("password"); + if (!password || typeof password !== "string") { + return new Response(null, { + status: 400 + }); + } + + const user = await db.table("user").where("email", "=", email).get(); + + if (!user) { + // NOTE: + // Returning immediately allows malicious actors to figure out valid emails from response times, + // allowing them to only focus on guessing passwords in brute-force attacks. + // As a preventive measure, you may want to hash passwords even for invalid emails. + // However, valid emails can be already be revealed with the signup page + // and a similar timing issue can likely be found in password reset implementation. + // It will also be much more resource intensive. + // Since protecting against this is none-trivial, + // it is crucial your implementation is protected against brute-force attacks with login throttling etc. + // If emails/usernames are public, you may outright tell the user that the username is invalid. + return new Response("Invalid email or password", { + status: 400 + }); + } + + const validPassword = await new Argon2id().verify(user.hashed_password, password); + if (!validPassword) { + return new Response("Invalid email or password", { + status: 400 + }); + } + + const session = await lucia.createSession(user.id, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + return new Response(null, { + status: 302, + headers: { + Location: "/", + "Set-Cookie": sessionCookie.serialize() + } + }); +}); +``` diff --git a/docs/pages/translations/ja/guides/email-and-password/email-verification-codes.md b/docs/pages/translations/ja/guides/email-and-password/email-verification-codes.md new file mode 100644 index 000000000..4f33b47cc --- /dev/null +++ b/docs/pages/translations/ja/guides/email-and-password/email-verification-codes.md @@ -0,0 +1,181 @@ +--- +title: "Email verification codes" +--- + +# Email verification codes + +## Update database + +### User table + +Add a `email_verified` column (boolean). + +```ts +import { Lucia } from "lucia"; + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: env === "PRODUCTION" // set `Secure` flag in HTTPS + } + }, + getUserAttributes: (attributes) => { + return { + emailVerified: attributes.email_verified, + email: attributes.email + }; + } +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: { + email: string; + email_verified: boolean; + }; + } +} +``` + +### Email verification code table + +Create a table for storing for email verification codes. + +| column | type | attributes | +| ------------ | -------- | ------------------- | +| `id` | any | auto increment, etc | +| `code` | `string` | | +| `user_id` | `string` | unique | +| `email` | `string` | | +| `expires_at` | `Date` | | + +## Generate verification code + +The code should be valid for few minutes and linked to a single email. + +```ts +import { TimeSpan, createDate } from "oslo"; +import { generateRandomString, alphabet } from "oslo/crypto"; + +async function generateEmailVerificationCode(userId: string, email: string): Promise { + await db.table("email_verification_code").where("user_id", "=", userId).deleteAll(); + const code = generateRandomString(8, alphabet("0-9")); + await db.table("email_verification_code").insert({ + user_id: userId, + email, + code, + expires_at: createDate(new TimeSpan(5, "m")) // 5 minutes + }); + return code; +} +``` + +You can also use alphanumeric codes. + +```ts +const code = generateRandomString(6, alphabet("0-9", "A-Z")); +``` + +When a user signs up, set `email_verified` to `false`, create and send a verification code, and create a new session. + +```ts +import { generateId } from "lucia"; +import { encodeHex } from "oslo/encoding"; + +app.post("/signup", async () => { + // ... + + const userId = generateId(); + + await db.table("user").insert({ + id: userId, + email, + hashed_password: hashedPassword, + email_verified: false + }); + + const verificationCode = await generateEmailVerificationCode(userId, email); + await verificationCode(email, verificationCode); + + const session = await lucia.createSession(userId, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + return new Response(null, { + status: 302, + headers: { + Location: "/", + "Set-Cookie": sessionCookie.serialize() + } + }); +}); +``` + +When resending verification emails, make sure to implement rate limiting based on user ID and IP address. + +## Verify code and email + +**Make sure to implement throttling to prevent brute-force attacks**. + +Validate the verification code by comparing it against your database and checking the expiration and email. Make sure to invalidate all user sessions. + +```ts +import { isWithinExpiration } from "oslo"; + +app.post("/email-verification", async () => { + // ... + const { user } = await lucia.validateSession(sessionId); + if (!user) { + return new Response(null, { + status: 401 + }); + } + const code = formData.get("code"); + // check for length + if (typeof code !== "string" || code.length !== 8) { + return new Response(null, { + status: 400 + }); + } + + await db.beginTransaction(); + const databaseCode = await db + .table("email_verification_code") + .where("user_id", "=", user.id) + .get(); + if (databaseCode) { + await db.table("email_verification_code").where("id", "=", databaseCode.id).delete(); + } + await db.commit(); + + if (!databaseCode || databaseCode.code !== code) { + return new Response(null, { + status: 400 + }); + } + if (!isWithinExpiration(databaseCode.expires_at)) { + return new Response(null, { + status: 400 + }); + } + if (!user || user.email !== databaseCode.email) { + return new Response(null, { + status: 400 + }); + } + + await lucia.invalidateUserSessions(user.id); + await db.table("user").where("id", "=", user.id).update({ + email_verified: true + }); + + const session = await lucia.createSession(user.id, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + return new Response(null, { + status: 302, + headers: { + Location: "/", + "Set-Cookie": sessionCookie.serialize() + } + }); +}); +``` diff --git a/docs/pages/translations/ja/guides/email-and-password/email-verification-links.md b/docs/pages/translations/ja/guides/email-and-password/email-verification-links.md new file mode 100644 index 000000000..52c132de1 --- /dev/null +++ b/docs/pages/translations/ja/guides/email-and-password/email-verification-links.md @@ -0,0 +1,159 @@ +--- +title: "Email verification links" +--- + +# Email verification links + +We recommend using [email verification codes](/guides/email-and-password/email-verification-codes) instead as it's more user-friendly. + +## Update database + +### User table + +Add a `email_verified` column (boolean). + +```ts +import { Lucia } from "lucia"; + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: env === "PRODUCTION" // set `Secure` flag in HTTPS + } + }, + getUserAttributes: (attributes) => { + return { + emailVerified: attributes.email_verified, + email: attributes.email + }; + } +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: { + email: string; + email_verified: boolean; + }; + } +} +``` + +### Email verification token table + +Create a table for storing for email verification tokens. + +| column | type | attributes | +| ------------ | -------- | ----------- | +| `id` | `string` | primary key | +| `user_id` | `string` | | +| `email` | `string` | | +| `expires_at` | `Date` | | + +## Create verification token + +The token should be valid for at most few hours and linked to a single email. + +```ts +import { TimeSpan, createDate } from "oslo"; + +async function createEmailVerificationToken(userId: string, email: string): Promise { + // optionally invalidate all existing tokens + await db.table("email_verification_token").where("user_id", "=", userId).deleteAll(); + const tokenId = generateId(40); + await db.table("email_verification_token").insert({ + id: tokenId, + user_id: userId, + email, + expires_at: createDate(new TimeSpan(2, "h")) + }); + return tokenId; +} +``` + +When a user signs up, set `email_verified` to `false`, create and send a verification token, and create a new session. You can either store the token as part of the pathname or inside the search params of the verification endpoint. + +```ts +import { generateId } from "lucia"; +import { encodeHex } from "oslo/encoding"; + +app.post("/signup", async () => { + // ... + + const userId = generateId(); + + await db.table("user").insert({ + id: userId, + email, + hashed_password: hashedPassword, + email_verified: false + }); + + const verificationToken = await createEmailVerificationToken(userId, email); + const verificationLink = "http://localhost:3000/email-verification/" + verificationToken; + await sendVerificationEmail(email, verificationLink); + + const session = await lucia.createSession(userId, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + return new Response(null, { + status: 302, + headers: { + Location: "/", + "Set-Cookie": sessionCookie.serialize() + } + }); +}); +``` + +When resending verification emails, make sure to implement rate limiting based on user ID and IP address. + +## Verify token and email + +Extract the email verification token from the URL and validate by checking the expiration date and email. If the token is valid, invalidate all existing user sessions and create a new session. Make sure to invalidate all user sessions. + +```ts +import { isWithinExpiration } from "oslo"; + +app.get("email-verification/:token", async () => { + // ... + + // check your framework's API + const verificationToken = params.token; + + await db.beginTransaction(); + const token = await db + .table("email_verification_token") + .where("id", "=", verificationToken) + .get(); + await db.table("email_verification_token").where("id", "=", verificationToken).delete(); + await db.commit(); + + if (!token || !isWithinExpiration(token.expires_at)) { + return new Response(null, { + status: 400 + }); + } + const user = await db.table("user").where("id", "=", token.user_id).get(); + if (!user || user.email !== token.email) { + return new Response(null, { + status: 400 + }); + } + + await lucia.invalidateUserSessions(user.id); + await db.table("user").where("id", "=", user.id).update({ + email_verified: true + }); + + const session = await lucia.createSession(user.id, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + return new Response(null, { + status: 302, + headers: { + Location: "/", + "Set-Cookie": sessionCookie.serialize() + } + }); +}); +``` diff --git a/docs/pages/translations/ja/guides/email-and-password/index.md b/docs/pages/translations/ja/guides/email-and-password/index.md new file mode 100644 index 000000000..d876ac776 --- /dev/null +++ b/docs/pages/translations/ja/guides/email-and-password/index.md @@ -0,0 +1,15 @@ +--- +title: "Email and password" +--- + +# Email and password + +Email-based auth requires a lot of components so be prepared to do some work! For a step-by-step, framework-specific tutorial to learn the basics of password-based auth and Lucia, see the [Username and password](/tutorials/username-and-password) tutorial. + +- [Password basics](/guides/email-and-password/basics) +- Email verification + - [Email verification codes](/guides/email-and-password/email-verification-codes) (preferred) + - [Email verification links](/guides/email-and-password/email-verification-links) +- [Password reset](/guides/email-and-password/password-reset) +- [Login throttling](/guides/email-and-password/login-throttling) +- [Two-factor authorization](/guides/email-and-password/2fa) diff --git a/docs/pages/translations/ja/guides/email-and-password/login-throttling.md b/docs/pages/translations/ja/guides/email-and-password/login-throttling.md new file mode 100644 index 000000000..f96bfc05f --- /dev/null +++ b/docs/pages/translations/ja/guides/email-and-password/login-throttling.md @@ -0,0 +1,7 @@ +--- +title: "Login throttling" +--- + +# Login throttling + +_Work in progress_ diff --git a/docs/pages/translations/ja/guides/email-and-password/password-reset.md b/docs/pages/translations/ja/guides/email-and-password/password-reset.md new file mode 100644 index 000000000..44092f559 --- /dev/null +++ b/docs/pages/translations/ja/guides/email-and-password/password-reset.md @@ -0,0 +1,118 @@ +--- +title: "Password reset" +--- + +# Password reset + +Allow users to reset their password by sending them a reset link to their inbox. + +## Update database + +Create a table for storing for password reset tokens. + +| column | type | attributes | +| ------------ | -------- | ----------- | +| `id` | `string` | primary key | +| `user_id` | `string` | | +| `expires_at` | `Date` | | + +## Create verification token + +The token should be valid for at most few hours. + +```ts +import { TimeSpan, createDate } from "oslo"; +import { generateId } from "lucia"; + +async function createPasswordResetToken(userId: string): Promise { + // optionally invalidate all existing tokens + await db.table("password_reset_token").where("user_id", "=", userId).deleteAll(); + const tokenId = generateId(40); + await db.table("password_reset_token").insert({ + id: tokenId, + user_id: userId, + expires_at: createDate(new TimeSpan(2, "h")) + }); + return tokenId; +} +``` + +When a user requests a password reset email, check if the email is valid and create a new link. + +```ts +import { generateId } from "lucia"; + +app.post("/reset-password", async () => { + let email: string; + + // ... + + const user = await db.table("user").where("email", "=", email).get(); + if (!user || !user.email_verified) { + return new Response("Invalid email", { + status: 400 + }); + } + + const verificationToken = await createPasswordResetToken(userId); + const verificationLink = "http://localhost:3000/reset-password/" + verificationToken; + + await sendPasswordResetToken(email, verificationLink); + return new Response(null, { + status: 200 + }); +}); +``` + +Make sure to implement rate limiting based on IP addresses. + +## Verify token + +Extract the verification token from the URL and validate by checking the expiration date. If the token is valid, invalidate all existing user sessions, update the database, and create a new session. + +```ts +import { isWithinExpirationDate } from "oslo"; +import { Argon2id } from "oslo/password"; + +app.post("/reset-password/:token", async () => { + let password = formData.get("password"); + if (typeof password !== "string" || password.length < 8) { + return new Response(null, { + status: 400 + }); + } + // check your framework's API + const verificationToken = params.token; + + // ... + + await db.beginTransaction(); + const token = await db.table("password_reset_token").where("id", "=", verificationToken).get(); + if (token) { + await db.table("password_reset_token").where("id", "=", verificationToken).delete(); + } + await db.commit(); + + if (!token || !isWithinExpirationDate(token.expires_at)) { + return new Response(null, { + status: 400 + }); + } + + await lucia.invalidateUserSessions(user.id); + const hashedPassword = await new Argon2id().hash(password); + await db.table("user").where("id", "=", user.id).update({ + hashed_password: hashedPassword + }); + + const session = await lucia.createSession(user.id, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + return new Response(null, { + status: 302, + headers: { + Location: "/", + "Set-Cookie": sessionCookie.serialize() + } + }); +}); +``` diff --git a/docs/pages/translations/ja/guides/improving-sessions.md b/docs/pages/translations/ja/guides/improving-sessions.md new file mode 100644 index 000000000..0105be940 --- /dev/null +++ b/docs/pages/translations/ja/guides/improving-sessions.md @@ -0,0 +1,7 @@ +--- +title: "Improving sessions" +--- + +# Improving sessions + +_Work in progress_ diff --git a/docs/pages/translations/ja/guides/oauth/account-linking.md b/docs/pages/translations/ja/guides/oauth/account-linking.md new file mode 100644 index 000000000..341c19ac8 --- /dev/null +++ b/docs/pages/translations/ja/guides/oauth/account-linking.md @@ -0,0 +1,93 @@ +--- +title: "Account linking" +--- + +# Account linking + +This guide uses the database schema shown in the [Multiple OAuth providers](/guides/oauth/multiple-providers) guide. + +## Automatic + +In general, you'd want to link accounts with the same email. Keep in mind that the email can be not verified and you should always assume it isn't. Make sure to verify that the email has been verified. + +```ts +import { generateId } from "lucia"; + +const tokens = await github.validateAuthorizationCode(code); +const userResponse = await fetch("https://api.github.com/user", { + headers: { + Authorization: `Bearer ${tokens.accessToken}` + } +}); +const githubUser = await userResponse.json(); + +const emailsResponse = await fetch("https://api.github.com/user/emails", { + headers: { + Authorization: `Bearer ${tokens.accessToken}` + } +}); +const emails = await emailsResponse.json(); + +const primaryEmail = emails.find((email) => email.primary) ?? null; +if (!primaryEmail) { + return new Response("No primary email address", { + status: 400 + }); +} +if (!primaryEmail.verified) { + return new Response("Unverified email", { + status: 400 + }); +} + +const existingUser = await db.table("user").where("email", "=", primaryEmail.email).get(); +if (existingUser) { + await db.table("oauth_account").insert({ + provider_id: "github", + provider_user_id: githubUser.id, + user_id: existingUser.id + }); +} else { + const userId = generateId(); + await db.beginTransaction(); + await db.table("user").insert({ + id: userId, + email: primaryEmail.email + }); + await db.table("oauth_account").insert({ + provider_id: "github", + provider_user_id: githubUser.id, + user_id: userId + }); + await db.commitTransaction(); +} +``` + +## Manual + +Another approach is to let users manually add OAuth accounts from their profile/settings page. You'd want to setup another OAuth flow, and instead of creating a new user, add a new OAuth account tied to the authenticated user. + +```ts +const { user } = await lucia.validateSession(); +if (!user) { + return new Response(null, { + status: 401 + }); +} + +const tokens = await github.validateAuthorizationCode(code); +const userResponse = await fetch("https://api.github.com/user", { + headers: { + Authorization: `Bearer ${tokens.accessToken}` + } +}); +const githubUser = await userResponse.json(); + +// TODO: check if github account is already linked to a user + +await db.table("oauth_account").insert({ + provider_id: "github", + provider_user_id: githubUser.id, + user_id: user.id +}); +``` diff --git a/docs/pages/translations/ja/guides/oauth/basics.md b/docs/pages/translations/ja/guides/oauth/basics.md new file mode 100644 index 000000000..b07e24861 --- /dev/null +++ b/docs/pages/translations/ja/guides/oauth/basics.md @@ -0,0 +1,183 @@ +--- +title: "OAuth basics" +--- + +# OAuth basics + +For a step-by-step, framework-specific tutorial, see the [GitHub OAuth](/tutorials) tutorial. + +We recommend using [Arctic](https://github.com/pilcrowonpaper/arctic) for implementing OAuth 2.0. It is a lightweight library that provides APIs for creating authorization URLs, validating callbacks, and refreshing access tokens. This is the easiest way to implement OAuth with Lucia and it supports most major providers. This page will use GitHub, and while most providers have similar APIs, there might be some minor differences between them. + +``` +npm install arctic +``` + +For this guide, the callback URL is `/login/github/callback`, for example `http://localhost:3000/login/github/callback`. + +## Update database + +Add a `username` and a unique `github_id` column to the user table. + +| column | type | attributes | +| ----------- | -------- | ---------- | +| `username` | `string` | | +| `github_id` | `number` | unique | + +Declare the type with `DatabaseUserAttributes` and add the attributes to the user object using the `getUserAttributes()` configuration. + +```ts +// auth.ts +import { Lucia } from "lucia"; + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: env === "PRODUCTION" // set `Secure` flag in HTTPS + } + }, + getUserAttributes: (attributes) => { + return { + githubId: attributes.github_id, + username: attributes.username + }; + } +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: { + github_id: number; + username: string; + }; + } +} +``` + +## Initialize OAuth provider + +Import `GitHub` from Arctic and initialize it with the client ID and secret. + +```ts +// auth.ts +import { GitHub } from "arctic"; + +export const github = new GitHub(clientId, clientSecret); +``` + +## Creating authorization URL + +Create a route to handle authorization. Generate a new state, create a new authorization URL with `createAuthorizationURL()`, store the state, and redirect the user to the authorization URL. The user will be prompted to sign in with GitHub. + +```ts +import { github } from "./auth.js"; +import { generateState } from "arctic"; +import { serializeCookie } from "oslo/cookie"; + +app.get("/login/github", async (): Promise => { + const state = generateState(); + const url = await github.createAuthorizationURL(state); + return new Response(null, { + status: 302, + headers: { + Location: url.toString(), + "Set-Cookie": serializeCookie("github_oauth_state", state, { + httpOnly: true, + secure: env === "PRODUCTION", // set `Secure` flag in HTTPS + maxAge: 60 * 10, // 10 minutes + path: "/" + }) + } + }); +}); +``` + +You can now create a sign in button with just an anchor tag. + +```html +Sign in with GitHub +``` + +## Validate callback + +In the callback route, first get the state from the cookie and the search params and compare them. Validate the authorization code in the search params with `validateAuthorizationCode()`. This will throw an [`OAuth2RequestError`](https://oslo.js.org/reference/oauth2/OAuth2RequestError) if the code or credentials are invalid. After validating the code, get the user's profile using the access token. Check if the user is already registered with the GitHub ID, and create a new user if they aren't. Finally, create a new session and set the session cookie. + +```ts +import { github, lucia } from "./auth.js"; +import { OAuth2RequestError } from "arctic"; +import { generateId } from "lucia"; +import { parseCookies } from "oslo/cookie"; + +app.get("/login/github/callback", async (request: Request): Promise => { + const cookies = parseCookies(request.headers.get("Cookie") ?? ""); + const stateCookie = cookies.get("github_oauth_state") ?? null; + + const url = new URL(request.url); + const state = url.searchParams.get("state"); + const code = url.searchParams.get("code"); + + // verify state + if (!state || !stateCookie || !code || stateCookie !== state) { + return new Response(null, { + status: 400 + }); + } + + try { + const tokens = await github.validateAuthorizationCode(code); + const githubUserResponse = await fetch("https://api.github.com/user", { + headers: { + Authorization: `Bearer ${tokens.accessToken}` + } + }); + const githubUserResult: GitHubUserResult = await githubUserResponse.json(); + + const existingUser = await db.table("user").where("github_id", "=", githubUserResult.id).get(); + + if (existingUser) { + const session = await lucia.createSession(existingUser.id, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + return new Response(null, { + status: 302, + headers: { + Location: "/", + "Set-Cookie": sessionCookie.serialize() + } + }); + } + + const userId = generateId(15); + await db.table("user").insert({ + id: userId, + username: githubUserResult.login, + github_id: githubUserResult.id + }); + + const session = await lucia.createSession(userId, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + return new Response(null, { + status: 302, + headers: { + Location: "/", + "Set-Cookie": sessionCookie.serialize() + } + }); + } catch (e) { + console.log(e); + if (e instanceof OAuth2RequestError) { + // bad verification code, invalid credentials, etc + return new Response(null, { + status: 400 + }); + } + return new Response(null, { + status: 500 + }); + } +}); + +interface GitHubUserResult { + id: number; + login: string; // username +} +``` diff --git a/docs/pages/translations/ja/guides/oauth/custom-providers.md b/docs/pages/translations/ja/guides/oauth/custom-providers.md new file mode 100644 index 000000000..78e2b13ea --- /dev/null +++ b/docs/pages/translations/ja/guides/oauth/custom-providers.md @@ -0,0 +1,96 @@ +--- +title: "Custom OAuth 2.0 providers" +--- + +# Custom OAuth 2.0 providers + +If you're looking to implement OAuth 2.0 for a provider that Arctic doesn't support, we recommend using Oslo's [`OAuth2Client`](https://oslo.js.org/reference/oauth2/OAuth2Client). + +## Initialization + +Pass your client ID and the provider's authorization and token endpoint to initialize the client. You can optionally pass the redirect URI. + +```ts +import { OAuth2Client } from "oslo/oauth2"; + +const authorizeEndpoint = "https://github.com/login/oauth/authorize"; +const tokenEndpoint = "https://github.com/login/oauth/access_token"; + +const oauth2Client = new OAuth2Client(clientId, authorizeEndpoint, tokenEndpoint, { + redirectURI: "http://localhost:3000/login/github/callback" +}); +``` + +## Create authorization URL + +Create an authorization URL with [`OAuth2Client.createAuthorizationURL()`](https://oslo.js.org/reference/oauth2/OAuth2Client/createAuthorizationURL). This optionally accepts a `state`, `codeVerifier` for PKCE flows, and `scope`. + +```ts +import { generateState, generateCodeVerifier } from "oslo/oauth2"; + +const state = generateState(); +const codeVerifier = generateCodeVerifier(); // for PKCE flow + +const url = await oauth2Client.createAuthorizationURL({ + state, + scope: ["user:email"], + codeVerifier +}); +``` + +## Validate authorization callback + +Use [`OAuth2Client.validateAuthorizationCode()`](https://oslo.js.org/reference/oauth2/OAuth2Client/validateAuthorizationCode) to validate authorization codes. By default, it sends the client secret, if provided, using the HTTP basic auth scheme. To send it inside the request body (i.e. search params), set the `authenticateWith` option to `"request_body"`. + +This throws an [`OAuth2RequestError`](https://oslo.js.org/reference/oauth2/OAuth2RequestError) on error responses. + +You can add additional response JSON fields by passing a type. + +```ts +try { + const { accessToken, refreshToken } = await oauth2Client.validateAuthorizationCode<{ + refreshToken: string; + }>(code, { + credentials: clientSecret, + authenticateWith: "request_body" + }); +} catch (e) { + if (e instanceof OAuth2RequestError) { + // see https://www.rfc-editor.org/rfc/rfc6749#section-5.2 + const { request, message, description } = e; + } + // unknown error +} +``` + +For PKCE flow, pass the `codeVerifier` as an option. + +```ts +await oauth2Client.validateAuthorizationCode<{ + refreshToken: string; +}>(code, { + credentials: clientSecret, + codeVerifier +}); +``` + +## Refresh access tokens + +Use [`OAuth2Client.validateAuthorizationCode()`](https://oslo.js.org/reference/oauth2/OAuth2Client/refreshAccessToken) to refresh an access token. The API is similar to `validateAuthorizationCode()` and it also throws an `OAuth2RequestError` on error responses. + +```ts +try { + const { accessToken, refreshToken } = await oauth2Client.refreshAccessToken<{ + refreshToken: string; + }>(code, { + credentials: clientSecret, + authenticateWith: "request_body" + }); +} catch (e) { + if (e instanceof OAuth2RequestError) { + // see https://www.rfc-editor.org/rfc/rfc6749#section-5.2 + const { request, message, description } = e; + } + // unknown error +} +``` diff --git a/docs/pages/translations/ja/guides/oauth/index.md b/docs/pages/translations/ja/guides/oauth/index.md new file mode 100644 index 000000000..18033dbe0 --- /dev/null +++ b/docs/pages/translations/ja/guides/oauth/index.md @@ -0,0 +1,15 @@ +--- +title: "OAuth" +--- + +# OAuth + +OAuth, or social sign in, is the easiest way to implement authentication as you won't have to worry about email verification, passwords, and two-factor authorization. + +For a step-by-step, framework-specific tutorial, see the [GitHub OAuth](/tutorials/github-oauth) tutorial. + +- [OAuth basics](/guides/oauth/basics) +- [Multiple OAuth providers](/guides/oauth/multiple-providers) +- [PKCE](/guides/oauth/pkce) +- [Account linking](/guides/oauth/account-linking) +- [Custom OAuth providers](/guides/oauth/custom-providers) diff --git a/docs/pages/translations/ja/guides/oauth/multiple-providers.md b/docs/pages/translations/ja/guides/oauth/multiple-providers.md new file mode 100644 index 000000000..085e4cd7e --- /dev/null +++ b/docs/pages/translations/ja/guides/oauth/multiple-providers.md @@ -0,0 +1,68 @@ +--- +title: "Multiple OAuth providers" +--- + +# Multiple OAuth providers + +## Database + +To support multiple OAuth sign-in methods, we can store the OAuth credentials in its own OAuth account table instead of the user table. Here, the combination of `provider_id` and `provider_user_id` should be unique (composite primary key). + +| column | type | description | +| ------------------ | -------- | -------------- | +| `provider_id` | `string` | OAuth provider | +| `provider_user_id` | `string` | OAuth user ID | +| `user_id` | `string` | user ID | + +Here's an example with SQLite: + +```sql +CREATE TABLE oauth_account { + provider_id TEXT NOT NULL, + provider_user_id TEXT NOT NULL, + user_id TEXT NOT NULL, + PRIMARY KEY (provider_id, provider_user_id), + FOREIGN KEY (user_id) REFERENCES user(id) +} +``` + +We can then remove the `github_id` column etc from the user table. + +## Validating callback + +Instead of the user table, we can now use the OAuth account table to check if a user is already registered. If not, in a transaction, create the user and OAuth account. + +```ts +const tokens = await githubAuth.validateAuthorizationCode(code); +const githubUser = await githubAuth.getUser(tokens.accessToken); + +const existingAccount = await db + .table("oauth_account") + .where("provider_id", "=", "github") + .where("provider_user_id", "=", githubUser.id) + .get(); + +if (existingAccount) { + const session = await lucia.createSession(existingAccount.user_id, {}); + + // ... +} + +const userId = generateId(15); + +await db.beginTransaction(); +await db.table("user").insert({ + id: userId, + username: github.login +}); +await db.table("oauth_account").insert({ + provider_id "github", + provider_user_id: githubUser.id, + user_id: userId +}); +await db.commit(); + +const session = await lucia.createSession(userId, {}); + +// ... +``` diff --git a/docs/pages/translations/ja/guides/oauth/pkce.md b/docs/pages/translations/ja/guides/oauth/pkce.md new file mode 100644 index 000000000..a284bc8d9 --- /dev/null +++ b/docs/pages/translations/ja/guides/oauth/pkce.md @@ -0,0 +1,73 @@ +--- +title: "PKCE flow" +--- + +# PKCE flow + +## Create authorization URL + +Create a code verifier with `generateCodeVerifier()`, pass it to `createAuthorizationURL()`, and store it as a cookie alongside the state. + +```ts +import { twitterAuth } from "./auth.js"; +import { generateState, generateCodeVerifier } from "arctic"; +import { serializeCookie } from "oslo/cookie"; + +app.get("/login/twitter", async (): Promise => { + const state = generateState(); + const codeVerifier = generateCodeVerifier(); + const url = await twitterAuth.createAuthorizationURL(codeVerifier, state); + + const headers = new Headers(); + headers.append( + "Set-Cookie", + serializeCookie("twitter_oauth_state", state, { + httpOnly: true, + secure: env === "PRODUCTION", // set `Secure` flag in HTTPS + maxAge: 60 * 10, // 10 minutes + path: "/" + }) + ); + headers.append( + "Set-Cookie", + serializeCookie("code_verifier", codeVerifier, { + httpOnly: true, + secure: env === "PRODUCTION", + maxAge: 60 * 10, + path: "/" + }) + ); + + // ... +}); +``` + +## Validate callback + +Get the code verifier stored as a cookie and use it alongside the authorization code to validate the callback. + +```ts +import { twitterAuth, lucia } from "./auth.js"; +import { parseCookies } from "oslo/cookie"; + +app.get("/login/twitter/callback", async (request: Request): Promise => { + const cookies = parseCookies(request.headers.get("Cookie") ?? ""); + const stateCookie = cookies.get("twitter_oauth_state") ?? null; + const codeVerifier = cookies.get("code_verifier") ?? null; + + const url = new URL(request.url); + const state = url.searchParams.get("state"); + const code = url.searchParams.get("code"); + + // verify state + if (!state || !stateCookie || !code || stateCookie !== state || !codeVerifier) { + return new Response(null, { + status: 400 + }); + } + + const tokens = await twitterAuth.validateAuthorizationCode(code, codeVerifier); + + // ... +}); +``` diff --git a/docs/pages/translations/ja/guides/passkeys.md b/docs/pages/translations/ja/guides/passkeys.md new file mode 100644 index 000000000..87d81c83d --- /dev/null +++ b/docs/pages/translations/ja/guides/passkeys.md @@ -0,0 +1,7 @@ +--- +title: "Passkeys" +--- + +# Passkeys + +_Work in progress_ diff --git a/docs/pages/translations/ja/guides/troubleshooting.md b/docs/pages/translations/ja/guides/troubleshooting.md new file mode 100644 index 000000000..9d9c32ae2 --- /dev/null +++ b/docs/pages/translations/ja/guides/troubleshooting.md @@ -0,0 +1,61 @@ +--- +title: "Troubleshooting" +--- + +# Troubleshooting + +Here are some common issues and how to resolve them. Feel free to ask for help in our Discord server. + +## `User` and `Session` are typed as `any` + +Make sure you've registered your types. Check that the `typeof lucia` is indeed an instance of `Lucia` (not a function that returns `Lucia`) and that there are no TS errors (including `@ts-ignore`) when declaring `Lucia`. `Register` must be an `interface`, not a `type`. + +```ts +import { Lucia } from "lucia"; + +const lucia = new Lucia(adapter, { + // no ts errors +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + } +} +``` + +## Session cookies are not set in `localhost` + +By default, session cookies have a `Secure` flag, which requires HTTPS. You can disable it for development with the `sessionCookie.attributes.secure` configuration. + +```ts +import { Lucia } from "lucia"; + +const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: !devMode // disable when `devMode` is `true` + } + } +}); +``` + +## Can't validate POST requests + +Check your CSRF protection implementation. If you're using the code provided by the documentation, check the `Origin` and `Host` header. The hostname must match exactly. You can add additional domains to the array to allow more domains. + +```ts +import { verifyRequestOrigin } from "lucia"; + +verifyRequestOrigin(originHeader, [hostHeader, "api.example.com" /*...*/]); +``` + +## `crypto` is not defined + +You're likely using a runtime that doesn't support the Web Crypto API, such as Node.js 18 and below. Polyfill it by importing `webcrypto`. + +```ts +import { webcrypto } from "node:crypto"; + +globalThis.crypto = webcrypto as Crypto; +``` diff --git a/docs/pages/translations/ja/guides/validate-bearer-tokens.md b/docs/pages/translations/ja/guides/validate-bearer-tokens.md new file mode 100644 index 000000000..97f8753c1 --- /dev/null +++ b/docs/pages/translations/ja/guides/validate-bearer-tokens.md @@ -0,0 +1,29 @@ +--- +title: "Validate bearer tokens" +--- + +# Validate bearer tokens + +For apps that can't use cookies, store the session ID in localstorage and send it to the server as a bearer token. + +```ts +fetch("https://api.example.com", { + headers: { + Authorization: `Bearer ${sessionId}` + } +}); +``` + +In the server, you can use [`Lucia.readBearerToken()`](/reference/main/Lucia/readBearerToken) to get the session ID from the authorization header and validate the session with [`Lucia.validateSession()`](/reference/main/Lucia/validateSession). + +```ts +const authorizationHeader = request.headers.get("Authorization"); +const sessionId = lucia.readBearerToken(authorizationHeader ?? ""); +if (!sessionId) { + return new Response(null, { + status: 401 + }); +} + +const { session, user } = await lucia.validateSession(sessionId); +``` diff --git a/docs/pages/translations/ja/guides/validate-session-cookies/astro.md b/docs/pages/translations/ja/guides/validate-session-cookies/astro.md new file mode 100644 index 000000000..967da98a2 --- /dev/null +++ b/docs/pages/translations/ja/guides/validate-session-cookies/astro.md @@ -0,0 +1,86 @@ +--- +title: "Validate session cookies in Astro" +--- + +# Validate session cookies in Astro + +**CSRF protection must be implemented when using cookies and forms.** This can be easily done by comparing the `Origin` and `Host` header. + +We recommend creating a middleware to validate requests and store the current user inside `locals`. You can get the cookie name with `Lucia.sessionCookieName` and validate the session cookie with `Lucia.validateSession()`. Make sure to delete the session cookie if it's invalid and create a new session cookie when the expiration gets extended, which is indicated by `Session.fresh`. + +```ts +// src/middleware.ts +import { lucia } from "./auth"; +import { verifyRequestOrigin } from "lucia"; +import { defineMiddleware } from "astro:middleware"; + +export const onRequest = defineMiddleware(async (context, next) => { + if (context.request.method !== "GET") { + const originHeader = request.headers.get("Origin"); + // NOTE: You may need to use `X-Forwarded-Host` instead + const hostHeader = request.headers.get("Host"); + if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { + return new Response(null, { + status: 403 + }); + } + } + + const sessionId = context.cookies.get(lucia.sessionCookieName)?.value ?? null; + if (!sessionId) { + context.locals.user = null; + context.locals.session = null; + return next(); + } + + const { session, user } = await lucia.validateSession(sessionId); + if (session && session.fresh) { + const sessionCookie = lucia.createSessionCookie(session.id); + context.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + } + if (!session) { + const sessionCookie = lucia.createBlankSessionCookie(); + context.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + } + context.locals.user = user; + context.locals.user = session; + return next(); +}); +``` + +Make sure sure to type `App.Locals` as well. + +```ts +// src/env.d.ts + +/// +declare namespace App { + interface Locals { + user: import("lucia").User; + session: import("lucia").Session; + } +} +``` + +This will allow you to access the current user inside `.astro` pages and API routes. + +```ts +--- +if (!Astro.locals.user) { + return Astro.redirect("/login") +} +--- +``` + +```ts +import { lucia } from "$lib/server/auth"; + +export function GET(context: APIContext): Promise { + if (!context.locals.user) { + return new Response(null, { + status: 401 + }); + } + // ... +} +``` diff --git a/docs/pages/translations/ja/guides/validate-session-cookies/elysia.md b/docs/pages/translations/ja/guides/validate-session-cookies/elysia.md new file mode 100644 index 000000000..83aa12567 --- /dev/null +++ b/docs/pages/translations/ja/guides/validate-session-cookies/elysia.md @@ -0,0 +1,81 @@ +--- +title: "Validate session cookies in Elysia" +--- + +# Validate session cookies in Elysia + +**CSRF protection must be implemented when using cookies and forms.** This can be easily done by comparing the `Origin` and `Host` header. + +We recommend creating a middleware to validate requests and store the current user inside `Context` with `App.derive()`. You can get the cookie name with `Lucia.sessionCookieName` and validate the session cookie with `Lucia.validateSession()`. Make sure to delete the session cookie if it's invalid and create a new session cookie when the expiration gets extended, which is indicated by `Session.fresh`. + +```ts +// src/middleware.ts +import { verifyRequestOrigin } from "lucia"; + +import type { User, Session } from "lucia"; + +const app = new Elysia().derive( + async ( + context + ): Promise<{ + user: User | null; + session: Session | null; + }> => { + // CSRF check + if (context.request.method !== "GET") { + const originHeader = context.request.headers.get("Origin"); + // NOTE: You may need to use `X-Forwarded-Host` instead + const hostHeader = context.request.headers.get("Host"); + if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { + return { + user: null, + session: null + }; + } + } + + // use headers instead of Cookie API to prevent type coercion + const cookieHeader = context.request.headers.get("Cookie") ?? ""; + const sessionId = lucia.readSessionCookie(cookieHeader); + if (!sessionId) { + return { + user: null, + session: null + }; + } + + const { session, user } = await lucia.validateSession(sessionId); + if (session && session.fresh) { + const sessionCookie = lucia.createSessionCookie(session.id); + context.cookie[sessionCookie.name].set({ + value: sessionCookie.value, + ...sessionCookie.attributes + }); + } + if (!session) { + const sessionCookie = lucia.createBlankSessionCookie(); + context.cookie[sessionCookie.name].set({ + value: sessionCookie.value, + ...sessionCookie.attributes + }); + } + return { + user, + session + }; + } +); +``` + +This will allow you to access the current user with `Context.user`. + +```ts +app.get("/", async (context) => { + if (!context.user) { + return new Response(null, { + status: 401 + }); + } + // ... +}); +``` diff --git a/docs/pages/translations/ja/guides/validate-session-cookies/express.md b/docs/pages/translations/ja/guides/validate-session-cookies/express.md new file mode 100644 index 000000000..85b736942 --- /dev/null +++ b/docs/pages/translations/ja/guides/validate-session-cookies/express.md @@ -0,0 +1,69 @@ +--- +title: "Validate session cookies in Express" +--- + +# Validate session cookies in Express + +**CSRF protection must be implemented when using cookies and forms.** This can be easily done by comparing the `Origin` and `Host` header. + +We recommend creating 2 middleware for CSRF protection and validating requests. You can get the cookie with `Lucia.readSessionCookie()` and validate the session cookie with `Lucia.validateSession()`. Make sure to delete the session cookie if it's invalid and create a new session cookie when the expiration gets extended, which is indicated by `Session.fresh`. + +```ts +// src/middleware.ts +import { lucia } from "./auth.js"; +import { verifyRequestOrigin } from "lucia"; + +import type { User } from "lucia"; + +app.use((req, res, next) => { + if (req.method === "GET") { + return next(); + } + const originHeader = req.headers.origin ?? null; + // NOTE: You may need to use `X-Forwarded-Host` instead + const hostHeader = req.headers.host ?? null; + if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { + return res.status(403).end(); + } +}); + +app.use((req, res, next) => { + const sessionId = lucia.readSessionCookie(req.headers.cookie ?? ""); + if (!sessionId) { + res.locals.user = null; + res.locals.session = null; + return next(); + } + + const { session, user } = await lucia.validateSession(sessionId); + if (session && session.fresh) { + res.appendHeader("Set-Cookie", lucia.createSessionCookie(session.id).serialize()); + } + if (!session) { + res.appendHeader("Set-Cookie", lucia.createSessionCookie(session.id).serialize()); + } + res.locals.user = user; + res.locals.session = session; + return next(); +}); + +declare global { + namespace Express { + interface Locals { + user: User | null; + session: Session | null; + } + } +} +``` + +This will allow you to access the current user with `res.locals`. + +```ts +app.get("/", (req, res) => { + if (!res.locals.user) { + return res.status(403).end(); + } + // ... +}); +``` diff --git a/docs/pages/translations/ja/guides/validate-session-cookies/hono.md b/docs/pages/translations/ja/guides/validate-session-cookies/hono.md new file mode 100644 index 000000000..fa2a6e3cb --- /dev/null +++ b/docs/pages/translations/ja/guides/validate-session-cookies/hono.md @@ -0,0 +1,75 @@ +--- +title: "Validate session cookies in Hono" +--- + +# Validate session cookies in Hono + +**CSRF protection must be implemented when using cookies and forms.** This can be easily done by comparing the `Origin` and `Host` header. + +We recommend creating 2 middleware for CSRF protection and validating requests. You can get the cookie name with `Lucia.sessionCookieName` and validate the session cookie with `Lucia.validateSession()`. Make sure to delete the session cookie if it's invalid and create a new session cookie when the expiration gets extended, which is indicated by `Session.fresh`. + +```ts +// src/middleware.ts +import { lucia } from "./auth.js"; +import { verifyRequestOrigin } from "lucia"; +import { getCookie } from "hono/cookie"; + +import type { User, Session } from "lucia"; + +const app = new Hono<{ + Variables: { + user: User | null; + session: Session | null; + }; +}>(); + +app.use("*", (c, next) => { + // CSRF middleware + if (c.req.method === "GET") { + return next(); + } + const originHeader = c.req.header("Origin"); + // NOTE: You may need to use `X-Forwarded-Host` instead + const hostHeader = c.req.header("Host"); + if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { + return c.body(null, 403); + } + return next(); +}); + +app.use("*", (c, next) => { + const sessionId = getCookie(lucia.sessionCookieName) ?? null; + if (!sessionId) { + c.set("user", null); + c.set("session", null); + return next(); + } + const { session, user } = await lucia.validateSession(sessionId); + if (session && session.fresh) { + // use `header()` instead of `setCookie()` to avoid TS errors + c.header("Set-Cookie", lucia.createSessionCookie(session.id).serialize(), { + append: true + }); + } + if (!session) { + c.header("Set-Cookie", lucia.createBlankSessionCookie().serialize(), { + append: true + }); + } + c.set("user", user); + c.set("session", session); + return next(); +}); +``` + +This will allow you to access the current user with `Context.get()`. + +```ts +app.get("/", async (c) => { + const user = c.get("user"); + if (!user) { + return c.body(null, 401); + } + // ... +}); +``` diff --git a/docs/pages/translations/ja/guides/validate-session-cookies/index.md b/docs/pages/translations/ja/guides/validate-session-cookies/index.md new file mode 100644 index 000000000..db6bdac92 --- /dev/null +++ b/docs/pages/translations/ja/guides/validate-session-cookies/index.md @@ -0,0 +1,69 @@ +--- +title: "Validate session cookies" +--- + +# Validate session cookies + +This guide is also available for: + +- [Astro](/guides/validate-session-cookies/astro) +- [Elysia](/guides/validate-session-cookies/elysia) +- [Express](/guides/validate-session-cookies/express) +- [Hono](/guides/validate-session-cookies/hono) +- [Next.js App router](/guides/validate-session-cookies/nextjs-app) +- [Next.js Pages router](/guides/validate-session-cookies/nextjs-pages) +- [Nuxt](/guides/validate-session-cookies/nuxt) +- [SolidStart](/guides/validate-session-cookies/solidstart) +- [SvelteKit](/guides/validate-session-cookies/sveltekit) + +**CSRF protection must be implemented when using cookies and forms.** This can be easily done by comparing the `Origin` and `Host` header. + +For non-GET requests, check the request origin. You can use `readSessionCookie()` to get the session cookie from a HTTP `Cookie` header, and validate it with `Lucia.validateSession()`. Make sure to delete the session cookie if it's invalid and create a new session cookie when the expiration gets extended, which is indicated by `Session.fresh`. + +```ts +import { verifyRequestOrigin } from "lucia"; + +// Only required in non-GET requests (POST, PUT, DELETE, PATCH, etc) +const originHeader = request.headers.get("Origin"); +// NOTE: You may need to use `X-Forwarded-Host` instead +const hostHeader = request.headers.get("Host"); +if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { + return new Response(null, { + status: 403 + }); +} + +const cookieHeader = request.headers.get("Cookie"); +const sessionId = lucia.readSessionCookie(cookieHeader ?? ""); +if (!sessionId) { + return new Response(null, { + status: 401 + }); +} + +const headers = new Headers(); + +const { session, user } = await lucia.validateSession(sessionId); +if (!session) { + const sessionCookie = lucia.createBlankSessionCookie(); + headers.append("Set-Cookie", sessionCookie.serialize()); +} + +if (session && session.fresh) { + const sessionCookie = lucia.createSessionCookie(session.id); + headers.append("Set-Cookie", sessionCookie.serialize()); +} +``` + +If your framework provides utilities for cookies, you can get the session cookie name with `Lucia.sessionCookieName`. + +```ts +const sessionId = getCookie(lucia.sessionCookieName); +``` + +When setting cookies you can get the cookies name, value, and attributes from the `Cookie` object. + +```ts +const sessionCookie = lucia.createSessionCookie(sessionId); +setCookie(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); +``` diff --git a/docs/pages/translations/ja/guides/validate-session-cookies/nextjs-app.md b/docs/pages/translations/ja/guides/validate-session-cookies/nextjs-app.md new file mode 100644 index 000000000..472678fd5 --- /dev/null +++ b/docs/pages/translations/ja/guides/validate-session-cookies/nextjs-app.md @@ -0,0 +1,99 @@ +--- +title: "Validate session cookies in Next.js App router" +--- + +# Validate session cookies in Next.js App router + +You can get the cookie name with `Lucia.sessionCookieName` and validate the session cookie with `Lucia.validateSession()`. Delete the session cookie if it's invalid and create a new session cookie when the expiration gets extended, which is indicated by `Session.fresh`. We have to wrap it inside a try/catch block since Next.js doesn't allow you to set cookies when rendering the page. This is a known issue but Vercel has yet to acknowledge or address the issue. + +We recommend wrapping the function with [`cache()`](https://nextjs.org/docs/app/building-your-application/caching#react-cache-function) so it can be called multiple times without incurring multiple database calls. + +**CSRF protection is only handled by Next.js when using form actions.** If you're using API routes, it must be implemented by yourself (see below). + +```ts +import { lucia } from "@/utils/auth"; +import { cookies } from "next/headers"; + +const getUser = cache(async () => { + const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null; + if (!sessionId) return null; + const { user, session } = await lucia.validateSession(sessionId); + try { + if (session && session.fresh) { + const sessionCookie = lucia.createSessionCookie(session.id); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + } + if (!session) { + const sessionCookie = lucia.createBlankSessionCookie(); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + } + } catch { + // Next.js throws error when attempting to set cookies when rendering page + } + return user; +}); +``` + +Set `sessionCookie.expires` option to `false` so the session cookie persists for a longer period. + +```ts +import { Lucia } from "lucia"; + +const lucia = new Lucia(adapter, { + sessionCookie: { + expires: false, + attributes: { + // set to `true` when using HTTPS + secure: process.env.NODE_ENV === "production" + } + } +}); +``` + +You can now use `getUser()` in server components, including server actions. + +```ts +// app/api/page.tsx +import { redirect } from "next/navigation"; + +async function Page() { + const user = await getUser(); + if (!user) { + redirect("/login"); + } + // ... + async function action() { + "use server"; + const user = await getUser(); + if (!user) { + redirect("/login"); + } + // ... + } + // ... +} +``` + +For API routes, since Next.js does not implement CSRF protection for API routes, **CSRF protection must be implemented when dealing with forms** if you're dealing with forms. This can be easily done by comparing the `Origin` and `Host` header. We recommend using middleware for this. + +```ts +// middleware.ts +import { verifyRequestOrigin } from "lucia"; +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +export async function middleware(request: NextRequest): Promise { + if (request.method === "GET") { + return NextResponse.next(); + } + const originHeader = request.headers.get("Origin"); + // NOTE: You may need to use `X-Forwarded-Host` instead + const hostHeader = request.headers.get("Host"); + if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { + return new NextResponse(null, { + status: 403 + }); + } + return NextResponse.next(); +} +``` diff --git a/docs/pages/translations/ja/guides/validate-session-cookies/nextjs-pages.md b/docs/pages/translations/ja/guides/validate-session-cookies/nextjs-pages.md new file mode 100644 index 000000000..5d5af47b4 --- /dev/null +++ b/docs/pages/translations/ja/guides/validate-session-cookies/nextjs-pages.md @@ -0,0 +1,84 @@ +--- +title: "Validate session cookies in Next.js Pages router" +--- + +# Validate session cookies in Next.js Pages router + +When working with cookies, **CSRF protection must be implemented**. This can be easily done by comparing the `Origin` and `Host` header. While CSRF protection is strictly not necessary when using JSON requests, it should be implemented in Next.js as it doesn't differentiate between JSON and form submissions. We recommend using middleware for this. + +```ts +// middleware.ts +import { verifyRequestOrigin } from "lucia"; +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +export async function middleware(request: NextRequest): Promise { + if (request.method === "GET") { + return NextResponse.next(); + } + const originHeader = request.headers.get("Origin"); + // NOTE: You may need to use `X-Forwarded-Host` instead + const hostHeader = request.headers.get("Host"); + if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { + return new NextResponse(null, { + status: 403 + }); + } + return NextResponse.next(); +} +``` + +You can get the cookie name with `Lucia.sessionCookieName` and validate the session cookie with `Lucia.validateSession()`. Make sure to delete the session cookie if it's invalid and create a new session cookie when the expiration gets extended, which is indicated by `Session.fresh`. + +```ts +import { verifyRequestOrigin } from "lucia"; + +import type { NextApiRequest, NextApiResponse } from "next"; + +async function validateRequest(req: NextApiRequest, res: NextApiResponse): Promise { + const sessionId = req.cookies.get(lucia.sessionCookieName); + if (!sessionId) { + return null; + } + const { session, user } = await lucia.validateSession(sessionId); + if (!session) { + res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize()); + } + if (session && session.fresh) { + res.setHeader("Set-Cookie", lucia.createSessionCookie(session.id).serialize()); + } + return user; +} +``` + +You can now get the current user inside `getServerSideProps()` by passing the request and response. + +```ts +import type { GetServerSidePropsContext } from "next"; + +export function getServerSideProps(context: GetServerSidePropsContext) { + const user = await validateRequest(context.req, context.res); + if (!user) { + return { + redirect: { + destination: "/login", + permanent: false + } + }; + } + // ... +} +``` + +```ts +import type { NextApiRequest, NextApiResponse } from "next"; + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const user = await validateRequest(req, res); + if (!user) { + return res.status(401).end(); + } +} + +export default handler; +``` diff --git a/docs/pages/translations/ja/guides/validate-session-cookies/nuxt.md b/docs/pages/translations/ja/guides/validate-session-cookies/nuxt.md new file mode 100644 index 000000000..9547bd803 --- /dev/null +++ b/docs/pages/translations/ja/guides/validate-session-cookies/nuxt.md @@ -0,0 +1,64 @@ +--- +title: "Validate session cookies in Nuxt" +--- + +# Validate session cookies in Nuxt + +**CSRF protection must be implemented when using cookies and forms.** This can be easily done by comparing the `Origin` and `Host` header. + +We recommend creating a middleware to validate requests and store the current user inside `context`. You can get the cookie name with `Lucia.sessionCookieName` and validate the session cookie with `Lucia.validateSession()`. Make sure to delete the session cookie if it's invalid and create a new session cookie when the expiration gets extended, which is indicated by `Session.fresh`. + +```ts +// server/middleware/auth.ts +import { verifyRequestOrigin } from "lucia"; + +import type { Session, User } from "lucia"; + +export default defineEventHandler(async (event) => { + if (event.method !== "GET") { + const originHeader = getHeader(event, "Origin") ?? null; + // NOTE: You may need to use `X-Forwarded-Host` instead + const hostHeader = getHeader(event, "Host") ?? null; + if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { + return event.node.res.writeHead(403).end(); + } + } + + const sessionId = getCookie(event, lucia.sessionCookieName) ?? null; + if (!sessionId) { + event.context.session = null; + event.context.user = null; + return; + } + + const { session, user } = await lucia.validateSession(sessionId); + if (session && session.fresh) { + appendResponseHeader(event, "Set-Cookie", lucia.createSessionCookie(session.id).serialize()); + } + if (!session) { + appendResponseHeader(event, "Set-Cookie", lucia.createBlankSessionCookie().serialize()); + } + event.context.session = session; + event.context.user = user; +}); + +declare module "h3" { + interface H3EventContext { + user: User | null; + session: Session | null; + } +} +``` + +This will allow you to access the current user inside API routes. + +```ts +export default defineEventHandler(async (event) => { + if (!event.context.user) { + throw createError({ + statusCode: 401 + }); + } + // ... +}); +``` diff --git a/docs/pages/translations/ja/guides/validate-session-cookies/solidstart.md b/docs/pages/translations/ja/guides/validate-session-cookies/solidstart.md new file mode 100644 index 000000000..f59be02a7 --- /dev/null +++ b/docs/pages/translations/ja/guides/validate-session-cookies/solidstart.md @@ -0,0 +1,70 @@ +--- +title: "Validate session cookies in SolidStart" +--- + +# Validate session cookies in SolidStart + +**CSRF protection must be implemented when using cookies and forms.** This can be easily done by comparing the `Origin` and `Host` header. + +We recommend creating a middleware to validate requests and store the current user inside `context`. You can get the cookie name with `Lucia.sessionCookieName` and validate the session cookie with `Lucia.validateSession()`. Make sure to delete the session cookie if it's invalid and create a new session cookie when the expiration gets extended, which is indicated by `Session.fresh`. + +```ts +// src/middleware.ts +import { createMiddleware, appendHeader, getCookie, getHeader } from "@solidjs/start/server"; +import { Session, User, verifyRequestOrigin } from "lucia"; +import { lucia } from "./lib/auth"; + +export default defineEventHandler((event) => { + if (context.request.method !== "GET") { + const originHeader = getHeader(event, "Origin") ?? null; + // NOTE: You may need to use `X-Forwarded-Host` instead + const hostHeader = getHeader(event, "Host") ?? null; + if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { + return event.node.res.writeHead(403).end(); + } + } + + const sessionId = getCookie(event, lucia.sessionCookieName) ?? null; + if (!sessionId) { + event.context.user = null; + return; + } + + const { session, user } = await lucia.validateSession(sessionId); + if (session && session.fresh) { + appendResponseHeader(event, "Set-Cookie", lucia.createSessionCookie(session.id).serialize()); + } + if (!session) { + appendResponseHeader(event, "Set-Cookie", lucia.createBlankSessionCookie().serialize()); + } + event.context.user = user; +}); + +declare module "vinxi/server" { + interface H3EventContext { + user: User | null; + session: Session | null; + } +} +``` + +Make sure to declare the middleware module in the config. + +```ts +// vite.config.ts +import { defineConfig } from "@solidjs/start/config"; + +export default defineConfig({ + start: { + middleware: "./src/middleware.ts" + } +}); +``` + +This will allow you to access the current user inside server contexts. + +```ts +import { getRequestEvent } from "solid-js/web"; + +const user = getRequestEvent()!.context.user; +``` diff --git a/docs/pages/translations/ja/guides/validate-session-cookies/sveltekit.md b/docs/pages/translations/ja/guides/validate-session-cookies/sveltekit.md new file mode 100644 index 000000000..2fb7c157e --- /dev/null +++ b/docs/pages/translations/ja/guides/validate-session-cookies/sveltekit.md @@ -0,0 +1,90 @@ +--- +title: "Validate session cookies in SvelteKit" +--- + +# Validate session cookies in SvelteKit + +SvelteKit has basic CSRF protection by default. We recommend creating a handle hook to validate requests and store the current user inside `locals`. You can get the cookie name with `Lucia.sessionCookieName` and validate the session cookie with `Lucia.validateSession()`. Make sure to delete the session cookie if it's invalid and create a new session cookie when the expiration gets extended, which is indicated by `Session.fresh`. + +```ts +// src/hooks.server.ts +import { lucia } from "$lib/server/auth"; + +import type { Handle } from "@sveltejs/kit"; + +export const handle: Handle = async ({ event, resolve }) => { + const sessionId = event.cookies.get(lucia.sessionCookieName); + if (!sessionId) { + event.locals.user = null; + event.locals.session = null; + return resolve(event); + } + + const { session, user } = await lucia.validateSession(sessionId); + if (session && session.fresh) { + const sessionCookie = lucia.createSessionCookie(session.id); + event.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + } + if (!session) { + const sessionCookie = lucia.createBlankSessionCookie(); + event.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + } + event.locals.user = user; + event.locals.session = session; + return resolve(event); +}; +``` + +Make sure sure to type `App.Locals` as well. + +```ts +// src/app.d.ts +declare global { + namespace App { + interface Locals { + user: import("lucia").User; + session: import("lucia").Session; + } + } +} +``` + +This will allow you to access the current user inside server load functions, actions, and API routes. + +```ts +// +page.server.ts +import { lucia } from "$lib/server/auth"; +import { fail, redirect } from "@sveltejs/kit"; + +import type { Actions, PageServerLoad } from "./$types"; + +export const load: PageServerLoad = async (event) => { + if (!event.locals.user) { + redirect("/login"); + } + // ... +}; + +export const actions: Actions = { + default: async (event) => { + if (!event.locals.user) { + throw fail(401); + } + // ... + } +}; +``` + +```ts +// +server.ts +import { lucia } from "$lib/server/auth"; + +export function GET(event: RequestEvent): Promise { + if (!event.locals.user) { + return new Response(null, { + status: 401 + }); + } + // ... +} +``` diff --git a/docs/pages/translations/ja/index.md b/docs/pages/translations/ja/index.md new file mode 100644 index 000000000..7efcb9b0b --- /dev/null +++ b/docs/pages/translations/ja/index.md @@ -0,0 +1,25 @@ +--- +title: "Lucia documentation" +--- + +# Lucia documentation + +Lucia is an auth library for your server that abstracts away the complexity of handling sessions. It works alongside your database to provide an API that's easy to use, understand, and extend. [Get started →](/getting-started) + +- No more endless configuration and callbacks +- Fully typed +- Works in any runtime - Node.js, Bun, Deno, Cloudflare Workers +- Extensive database support out of the box + +```ts +import { Lucia } from "lucia"; + +const lucia = new Lucia(new Adapter(db)); + +const session = await lucia.createSession(userId, {}); +await lucia.validateSession(session.id); +``` + +Lucia is an open source library released under the MIT license, with the help of [100+ contributors](https://github.com/lucia-auth/lucia/graphs/contributors)! Join us on [Discord](https://discord.com/invite/PwrK3kpVR3) if you have any questions. + +> In case you missed the news, we've recently released Lucia 3.0! [Read the announcement](https://github.com/lucia-auth/lucia/discussions/1361). diff --git a/docs/pages/translations/ja/main/Adapter.md b/docs/pages/translations/ja/main/Adapter.md new file mode 100644 index 000000000..654a9a53c --- /dev/null +++ b/docs/pages/translations/ja/main/Adapter.md @@ -0,0 +1,35 @@ +--- +title: "Adapter" +--- + +# `Adapter` + +Represents a database adapter. + +## Definition + +```ts +//$ DatabaseSession=/reference/main/DatabaseSession +//$ DatabaseUser=/reference/main/DatabaseUser +interface Adapter { + deleteExpiredSessions(): Promise; + deleteSession(sessionId: string): Promise; + deleteUserSessions(userId: string): Promise; + getSessionAndUser( + sessionId: string + ): Promise<[session: $$DatabaseSession | null, user: $$DatabaseUser | null]>; + getUserSessions(userId: string): Promise<$$DatabaseSession[]>; + setSession(session: $$DatabaseSession): Promise; + updateSessionExpiration(sessionId: string, expiresAt: Date): Promise; +} +``` + +### Methods + +- `deleteExpiredSessions`: Deletes all sessions where `expires_at` is equal to or less than current timestamp (machine time) +- `deleteSession()`: Deletes the session +- `deleteUserSessions()`: Deletes all sessions linked to the user +- `getSessionAndUser()`: Returns the session and the user linked to the session +- `getUserSessions()`: Returns all sessions linked to a user +- `setSession()`: Inserts the session +- `updateSessionExpiration()`: Updates the `expires_at` field of the session diff --git a/docs/pages/translations/ja/main/Cookie.md b/docs/pages/translations/ja/main/Cookie.md new file mode 100644 index 000000000..690b9b75f --- /dev/null +++ b/docs/pages/translations/ja/main/Cookie.md @@ -0,0 +1,7 @@ +--- +title: "Cookie" +--- + +# `Cookie` + +See [`Cookie`](https://oslo.js.org/reference/cookie/Cookie) from `oslo/cookie`. diff --git a/docs/pages/translations/ja/main/DatabaseSession.md b/docs/pages/translations/ja/main/DatabaseSession.md new file mode 100644 index 000000000..84da0a24b --- /dev/null +++ b/docs/pages/translations/ja/main/DatabaseSession.md @@ -0,0 +1,26 @@ +--- +title: "DatabaseSession" +--- + +# `DatabaseSession` + +Represents a session stored in a database. + +## Definition + +```ts +//$ DatabaseSessionAttributes=/reference/main/DatabaseSessionAttributes +interface DatabaseSession { + id: string; + userId: string; + expiresAt: Date; + attributes: $$DatabaseSessionAttributes; +} +``` + +### Properties + +- `id` +- `userId` +- `expiresAt` +- `attributes` diff --git a/docs/pages/translations/ja/main/DatabaseSessionAttributes.md b/docs/pages/translations/ja/main/DatabaseSessionAttributes.md new file mode 100644 index 000000000..b18e40bb8 --- /dev/null +++ b/docs/pages/translations/ja/main/DatabaseSessionAttributes.md @@ -0,0 +1,13 @@ +--- +title: "DatabaseSessionAttributes" +--- + +# `DatabaseSessionAttributes` + +Additional data stored in the session table. + +## Definition + +```ts +interface DatabaseSessionAttributes {} +``` diff --git a/docs/pages/translations/ja/main/DatabaseUser.md b/docs/pages/translations/ja/main/DatabaseUser.md new file mode 100644 index 000000000..bddef69b9 --- /dev/null +++ b/docs/pages/translations/ja/main/DatabaseUser.md @@ -0,0 +1,24 @@ +--- +title: "DatabaseUser" +--- + +# `DatabaseUser` + +Represents a session stored in a database. + +## Definition + +```ts +//$ DatabaseUserAttributes=/reference/main/DatabaseUserAttributes +interface DatabaseUser { + id: string; + attributes: DatabaseUserAttributes; +} +``` + +### Properties + +- `id` +- `userId` +- `expiresAt` +- `attributes` diff --git a/docs/pages/translations/ja/main/DatabaseUserAttributes.md b/docs/pages/translations/ja/main/DatabaseUserAttributes.md new file mode 100644 index 000000000..e7a31a962 --- /dev/null +++ b/docs/pages/translations/ja/main/DatabaseUserAttributes.md @@ -0,0 +1,13 @@ +--- +title: "DatabaseUserAttributes" +--- + +# `DatabaseUserAttributes` + +Additional data stored in the user table. + +## Definition + +```ts +interface DatabaseUserAttributes {} +``` diff --git a/docs/pages/translations/ja/main/LegacyScrypt/hash.md b/docs/pages/translations/ja/main/LegacyScrypt/hash.md new file mode 100644 index 000000000..81a0852f0 --- /dev/null +++ b/docs/pages/translations/ja/main/LegacyScrypt/hash.md @@ -0,0 +1,23 @@ +--- +title: "LegacyScrypt.hash()" +--- + +# `LegacyScrypt.hash()` + +Method of [`LegacyScrypt`](/reference/main/LegacyScrypt). Hashes the provided password with scrypt. + +## Definition + +```ts +function hash(password: string): Promise; +``` + +### Parameters + +- `password` + +## Example + +```ts +const hash = await scrypt.hash(password); +``` diff --git a/docs/pages/translations/ja/main/LegacyScrypt/index.md b/docs/pages/translations/ja/main/LegacyScrypt/index.md new file mode 100644 index 000000000..2bedc39a6 --- /dev/null +++ b/docs/pages/translations/ja/main/LegacyScrypt/index.md @@ -0,0 +1,38 @@ +--- +title: "LegacyScrypt" +--- + +# `LegacyScrypt` + +A pure JS implementation of Scrypt for projects that used Lucia v1/v2. For new projects, use [`Scrypt`](/reference/main/Scrypt). + +The output hash is a combination of the scrypt hash and the 32-bytes salt, in the format of `:`. + +## Constructor + +```ts +function constructor(options?: { N?: number; r?: number; p?: number; dkLen?: number }): this; +``` + +### Parameters + +- `options` + - `N` (default: `16384`) + - `r` (default: `16`) + - `p` (default: `1`) + - `dkLen` (default: `64`) + +## Methods + +- [`hash()`](/reference/main/LegacyScrypt/hash) +- [`verify()`](/reference/main/LegacyScrypt/verify) + +## Example + +```ts +import { LegacyScrypt } from "lucia"; + +const scrypt = new LegacyScrypt(); +const hash = await scrypt.hash(password); +const validPassword = await scrypt.verify(hash, password); +``` diff --git a/docs/pages/translations/ja/main/LegacyScrypt/verify.md b/docs/pages/translations/ja/main/LegacyScrypt/verify.md new file mode 100644 index 000000000..540868731 --- /dev/null +++ b/docs/pages/translations/ja/main/LegacyScrypt/verify.md @@ -0,0 +1,24 @@ +--- +title: "LegacyScrypt.verify()" +--- + +# `LegacyScrypt.verify()` + +Method of [`LegacyScrypt`](/reference/main/LegacyScrypt). Verifies the password with the hash using scrypt. + +## Definition + +```ts +function verify(hash: string, password: string): Promise; +``` + +### Parameters + +- `hash` +- `password` + +## Example + +```ts +const validPassword = await scrypt.verify(hash, password); +``` diff --git a/docs/pages/translations/ja/main/Lucia/createBlankSessionCookie.md b/docs/pages/translations/ja/main/Lucia/createBlankSessionCookie.md new file mode 100644 index 000000000..d97eb491c --- /dev/null +++ b/docs/pages/translations/ja/main/Lucia/createBlankSessionCookie.md @@ -0,0 +1,14 @@ +--- +title: "Lucia.createBlankSessionCookie()" +--- + +# `Lucia.createBlankSessionCookie()` + +Method of [`Lucia`](/reference/main/Lucia). Creates a new cookie with a blank value that expires immediately to delete the existing session cookie. + +## Definition + +```ts +//$ Cookie=/reference/cookie/Cookie +function createBlankSessionCookie(): $$Cookie; +``` diff --git a/docs/pages/translations/ja/main/Lucia/createSession.md b/docs/pages/translations/ja/main/Lucia/createSession.md new file mode 100644 index 000000000..28f6b20a4 --- /dev/null +++ b/docs/pages/translations/ja/main/Lucia/createSession.md @@ -0,0 +1,20 @@ +--- +title: "Lucia.createSession()" +--- + +# `Lucia.createSession()` + +Method of [`Lucia`](/reference/main/Lucia). Creates a new session. + +## Definition + +```ts +//$ DatabaseSessionAttributes=/reference/main/DatabaseSessionAttributes +//$ Session=/reference/main/Session +function createSession(userId: string, attributes: $$DatabaseSessionAttributes): Promise<$$Session>; +``` + +### Parameters + +- `userId` +- `attributes`: Database session attributes diff --git a/docs/pages/translations/ja/main/Lucia/createSessionCookie.md b/docs/pages/translations/ja/main/Lucia/createSessionCookie.md new file mode 100644 index 000000000..085cbd38e --- /dev/null +++ b/docs/pages/translations/ja/main/Lucia/createSessionCookie.md @@ -0,0 +1,14 @@ +--- +title: "Lucia.createSessionCookie()" +--- + +# `Lucia.createSessionCookie()` + +Method of [`Lucia`](/reference/main/Lucia). Creates a new session cookie. + +## Definition + +```ts +//$ Cookie=/reference/cookie/Cookie +function createSessionCookie(sessionId: string): $$Cookie; +``` diff --git a/docs/pages/translations/ja/main/Lucia/deleteExpiredSessions.md b/docs/pages/translations/ja/main/Lucia/deleteExpiredSessions.md new file mode 100644 index 000000000..5ecfc8529 --- /dev/null +++ b/docs/pages/translations/ja/main/Lucia/deleteExpiredSessions.md @@ -0,0 +1,13 @@ +--- +title: "Lucia.deleteExpiredSessions()" +--- + +# `Lucia.deleteExpiredSessions()` + +Method of [`Lucia`](/reference/main/Lucia). Deletes all expired sessions. + +## Definition + +```ts +function deleteExpiredSessions(): Promise; +``` diff --git a/docs/pages/translations/ja/main/Lucia/getUserSessions.md b/docs/pages/translations/ja/main/Lucia/getUserSessions.md new file mode 100644 index 000000000..43d12b9e4 --- /dev/null +++ b/docs/pages/translations/ja/main/Lucia/getUserSessions.md @@ -0,0 +1,18 @@ +--- +title: "Lucia.getUserSessions()" +--- + +# `Lucia.getUserSessions()` + +Method of [`Lucia`](/reference/main/Lucia). Gets all sessions of a user. + +## Definition + +```ts +//$ Session=/reference/main/Session +function getUserSessions(userId: string): Promise; +``` + +### Parameters + +- `userId` diff --git a/docs/pages/translations/ja/main/Lucia/index.md b/docs/pages/translations/ja/main/Lucia/index.md new file mode 100644 index 000000000..9084ef7fc --- /dev/null +++ b/docs/pages/translations/ja/main/Lucia/index.md @@ -0,0 +1,68 @@ +--- +title: "Lucia" +--- + +# `Lucia` + +## Constructor + +```ts +//$ Adapter=/reference/main/Adapter +//$ TimeSpan=/reference/main/TimeSpan +//$ DatabaseSessionAttributes=/reference/main/DatabaseSessionAttributes +//$ DatabaseUserAttributes=/reference/main/DatabaseUserAttributes +function constructor< + _SessionAttributes extends {} = Record, + _UserAttributes extends {} = Record +>( + adapter: $$Adapter, + options?: { + sessionExpiresIn?: $$TimeSpan; + sessionCookie?: { + name?: string; + expires?: boolean; + attributes: { + sameSite?: "lax" | "strict"; + domain?: string; + path?: string; + secure?: boolean; + }; + }; + getSessionAttributes?: ( + databaseSessionAttributes: $$DatabaseSessionAttributes + ) => _SessionAttributes; + getUserAttributes?: (databaseUserAttributes: $$DatabaseUserAttributes) => _UserAttributes; + } +): this; +``` + +### Parameters + +- `adapter`: Database adapter +- `options`: + - `sessionExpiresIn`: How long a session lasts for inactive users + - `sessionCookie`: Session cookie options + - `name`: Cookie name (default: `auth_session`) + - `expires`: Set to `false` for cookies to persist indefinitely (default: `true`) + - `attributes`: Cookie attributes + - `sameSite` + - `domain` + - `path` + - `secure` + - `getSessionAttributes()`: Transforms database session attributes and the returned object is added to the [`Session`](/reference/main/Session) object + - `getUserAttributes()`: Transforms database user attributes and the returned object is added to the [`User`](/reference/main/User) object + +## Method + +- [`createBlankSessionCookie()`](/reference/main/Lucia/createBlankSessionCookie) +- [`createSession()`](/reference/main/Lucia/createSession) +- [`createSessionCookie()`](/reference/main/Lucia/createSessionCookie) +- [`deleteExpiredSessions()`](/reference/main/Lucia/deleteExpiredSessions) +- [`getUserSessions()`](/reference/main/Lucia/getUserSessions) +- [`handleRequest()`](/reference/main/Lucia/handleRequest) +- [`createSessionCookie()`](/reference/main/Lucia/createSessionCookie) +- [`invalidateSession()`](/reference/main/Lucia/invalidateSession) +- [`invalidateUserSessions()`](/reference/main/Lucia/invalidateUserSessions) +- [`readBearerToken()`](/reference/main/Lucia/readBearerToken) +- [`readSessionCookie()`](/reference/main/Lucia/readSessionCookie) +- [`validateSession()`](/reference/main/Lucia/validateSession) diff --git a/docs/pages/translations/ja/main/Lucia/invalidateSession.md b/docs/pages/translations/ja/main/Lucia/invalidateSession.md new file mode 100644 index 000000000..8b53942a4 --- /dev/null +++ b/docs/pages/translations/ja/main/Lucia/invalidateSession.md @@ -0,0 +1,17 @@ +--- +title: "Lucia.invalidateSession()" +--- + +# `Lucia.invalidateSession()` + +Method of [`Lucia`](/reference/main/Lucia). Invalidates a session. + +## Definition + +```ts +function invalidateSession(sessionId: string): Promise; +``` + +### Parameters + +- `sessionId` diff --git a/docs/pages/translations/ja/main/Lucia/invalidateUserSessions.md b/docs/pages/translations/ja/main/Lucia/invalidateUserSessions.md new file mode 100644 index 000000000..1032a01b3 --- /dev/null +++ b/docs/pages/translations/ja/main/Lucia/invalidateUserSessions.md @@ -0,0 +1,17 @@ +--- +title: "Lucia.invalidateUserSessions()" +--- + +# `Lucia.invalidateUserSessions()` + +Method of [`Lucia`](/reference/main/Lucia). Invalidates all sessions of a user. + +## Definition + +```ts +function invalidateUserSessions(userId: string): Promise; +``` + +### Parameters + +- `userId` diff --git a/docs/pages/translations/ja/main/Lucia/readBearerToken.md b/docs/pages/translations/ja/main/Lucia/readBearerToken.md new file mode 100644 index 000000000..733838bbf --- /dev/null +++ b/docs/pages/translations/ja/main/Lucia/readBearerToken.md @@ -0,0 +1,17 @@ +--- +title: "Lucia.readBearerToken()" +--- + +# `Lucia.readBearerToken()` + +Method of [`Lucia`](/reference/main/Lucia). Reads the bearer token from the ` Authorization`` header. Returns `null` if the token doesn't exist. + +## Definition + +```ts +function readBearerToken(authorizationHeader: string): string | null; +``` + +### Parameters + +- `authorizationHeader`: HTTP `Authorization` header diff --git a/docs/pages/translations/ja/main/Lucia/readSessionCookie.md b/docs/pages/translations/ja/main/Lucia/readSessionCookie.md new file mode 100644 index 000000000..86d0819c9 --- /dev/null +++ b/docs/pages/translations/ja/main/Lucia/readSessionCookie.md @@ -0,0 +1,17 @@ +--- +title: "Lucia.readSessionCookie()" +--- + +# `Lucia.readSessionCookie()` + +Method of [`Lucia`](/reference/main/Lucia). Reads the session cookie from the `Cookie` header. Returns `null` if the cookie doesn't exist. + +## Definition + +```ts +function readSessionCookie(cookieHeader: string): string | null; +``` + +### Parameters + +- `cookieHeader`: HTTP `Cookie` header diff --git a/docs/pages/translations/ja/main/Lucia/validateSession.md b/docs/pages/translations/ja/main/Lucia/validateSession.md new file mode 100644 index 000000000..245a0f307 --- /dev/null +++ b/docs/pages/translations/ja/main/Lucia/validateSession.md @@ -0,0 +1,21 @@ +--- +title: "Lucia.validateSession()" +--- + +# `Lucia.validateSession()` + +Method of [`Lucia`](/reference/main/Lucia). Validates a session with the session ID. Extends the session expiration if in idle state. + +## Definition + +```ts +//$ User=/reference/main/User +//$ Session=/reference/main/Session +function validateSession( + sessionId: string +): Promise<{ user: $$User; session: $$Session } | { user: null; session: null }>; +``` + +### Parameters + +- `sessionId` diff --git a/docs/pages/translations/ja/main/Scrypt/hash.md b/docs/pages/translations/ja/main/Scrypt/hash.md new file mode 100644 index 000000000..5fb91796d --- /dev/null +++ b/docs/pages/translations/ja/main/Scrypt/hash.md @@ -0,0 +1,23 @@ +--- +title: "Scrypt.hash()" +--- + +# `Scrypt.hash()` + +Method of [`Scrypt`](/reference/main/Scrypt). Hashes the provided password with scrypt. + +## Definition + +```ts +function hash(password: string): Promise; +``` + +### Parameters + +- `password` + +## Example + +```ts +const hash = await scrypt.hash(password); +``` diff --git a/docs/pages/translations/ja/main/Scrypt/index.md b/docs/pages/translations/ja/main/Scrypt/index.md new file mode 100644 index 000000000..a00120442 --- /dev/null +++ b/docs/pages/translations/ja/main/Scrypt/index.md @@ -0,0 +1,40 @@ +--- +title: "Scrypt" +--- + +# `Scrypt` + +A pure JS implementation of Scrypt. Provides methods for hashing passwords and verifying hashes with [scrypt](https://datatracker.ietf.org/doc/html/rfc7914). By default, the configuration is set to [the recommended values](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html). + +The output hash is a combination of the scrypt hash and the 32-bytes salt, in the format of `:`. + +Since it's pure JS, it is anywhere from 2~3 times slower than implementations based on native code. See Oslo's [`Scrypt`](https://oslo.js.org/reference/password/Scrypt) for a faster API (Node.js-only). + +## Constructor + +```ts +function constructor(options?: { N?: number; r?: number; p?: number; dkLen?: number }): this; +``` + +### Parameters + +- `options` + - `N` (default: `16384`) + - `r` (default: `16`) + - `p` (default: `1`) + - `dkLen` (default: `64`) + +## Methods + +- [`hash()`](ref:password/Argon2id) +- [`verify()`](ref:password/Argon2id) + +## Example + +```ts +import { Scrypt } from "lucia"; + +const scrypt = new Scrypt(); +const hash = await scrypt.hash(password); +const validPassword = await scrypt.verify(hash, password); +``` diff --git a/docs/pages/translations/ja/main/Scrypt/verify.md b/docs/pages/translations/ja/main/Scrypt/verify.md new file mode 100644 index 000000000..05eb8d6d8 --- /dev/null +++ b/docs/pages/translations/ja/main/Scrypt/verify.md @@ -0,0 +1,24 @@ +--- +title: "Scrypt.verify()" +--- + +# `Scrypt.verify()` + +Method of [`Scrypt`](/reference/main/Scrypt). Verifies the password with the hash using scrypt. + +## Definition + +```ts +function verify(hash: string, password: string): Promise; +``` + +### Parameters + +- `hash` +- `password` + +## Example + +```ts +const validPassword = await scrypt.verify(hash, password); +``` diff --git a/docs/pages/translations/ja/main/Session.md b/docs/pages/translations/ja/main/Session.md new file mode 100644 index 000000000..f16b659c5 --- /dev/null +++ b/docs/pages/translations/ja/main/Session.md @@ -0,0 +1,26 @@ +--- +title: "Session" +--- + +# `Session` + +Represents a session. + +## Definition + +```ts +//$ SessionAttributes=/reference/main/SessionAttributes +interface Session extends SessionAttributes { + id: string; + expiresAt: Date; + fresh: boolean; + userId: string; +} +``` + +### Properties + +- `id` +- `expiresAt` +- `fresh`: `true` if session was newly created or its expiration was extended +- `userId` diff --git a/docs/pages/translations/ja/main/TimeSpan.md b/docs/pages/translations/ja/main/TimeSpan.md new file mode 100644 index 000000000..3d8d3ad5c --- /dev/null +++ b/docs/pages/translations/ja/main/TimeSpan.md @@ -0,0 +1,7 @@ +--- +title: "TimeSpan" +--- + +# `TimeSpan` + +See [`TimeSpan`](https://oslo.js.org/reference/main/TimeSpan) from `oslo`. diff --git a/docs/pages/translations/ja/main/User.md b/docs/pages/translations/ja/main/User.md new file mode 100644 index 000000000..643e719c0 --- /dev/null +++ b/docs/pages/translations/ja/main/User.md @@ -0,0 +1,20 @@ +--- +title: "User" +--- + +# `User` + +Represents a user. + +## Definition + +```ts +//$ UserAttributes=/reference/main/UserAttributes +interface User extends UserAttributes { + id: string; +} +``` + +### Properties + +- `id` diff --git a/docs/pages/translations/ja/main/generateId.md b/docs/pages/translations/ja/main/generateId.md new file mode 100644 index 000000000..2fed737fd --- /dev/null +++ b/docs/pages/translations/ja/main/generateId.md @@ -0,0 +1,26 @@ +--- +title: "generateId()" +--- + +# `generateId()` + +Generates a cryptographically strong random string made of `a-z` (lowercase) and `0-9`. + +## Definition + +```ts +function generateId(length: number): string; +``` + +### Parameters + +- `length` + +## Example + +```ts +import { generateId } from "lucia"; + +// 10-characters long string +generateId(10); +``` diff --git a/docs/pages/translations/ja/main/index.md b/docs/pages/translations/ja/main/index.md new file mode 100644 index 000000000..adb462beb --- /dev/null +++ b/docs/pages/translations/ja/main/index.md @@ -0,0 +1,27 @@ +--- +title: "lucia API reference" +--- + +# `lucia` API reference + +## Functions + +- [`generateId()`](/reference/main/generateId) + +## Classes + +- [`LegacyScrypt`](/reference/main/LegacyScrypt) +- [`Lucia`](/reference/main/Lucia) +- [`Cookie`](/reference/main/SessionCookie) +- [`Scrypt`](/reference/main/Scrypt) +- [`TimeSpan`](/reference/main/TimeSpan) + +## Interfaces + +- [`Adapter`](/reference/main/Adapter) +- [`DatabaseSession`](/reference/main/DatabaseSession) +- [`DatabaseSessionAttributes`](/reference/main/DatabaseSessionAttributes) +- [`DatabaseUser`](/reference/main/DatabaseUser) +- [`DatabaseUserAttributes`](/reference/main/DatabaseUserAttributes) +- [`Session`](/reference/main/Session) +- [`User`](/reference/main/User) diff --git a/docs/pages/translations/ja/main/verifyRequestOrigin.md b/docs/pages/translations/ja/main/verifyRequestOrigin.md new file mode 100644 index 000000000..51803964c --- /dev/null +++ b/docs/pages/translations/ja/main/verifyRequestOrigin.md @@ -0,0 +1,7 @@ +--- +title: "verifyRequestOrigin()" +--- + +# `Cookie` + +See [`verifyRequestOrigin()`](https://oslo.js.org/reference/request/verifyRequestOrigin) from `oslo/request`. diff --git a/docs/pages/translations/ja/tutorials/github-oauth/astro.md b/docs/pages/translations/ja/tutorials/github-oauth/astro.md new file mode 100644 index 000000000..02fa0f571 --- /dev/null +++ b/docs/pages/translations/ja/tutorials/github-oauth/astro.md @@ -0,0 +1,237 @@ +--- +title: "GitHub OAuth in Astro" +--- + +# Tutorial: GitHub OAuth in Astro + +Before starting, make sure you've set up your database and middleware as described in the [Getting started](/getting-started/astro) page. + +An [example project](https://github.com/lucia-auth/examples/tree/main/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/examples/tree/v3/astro/github-oauth). + +``` +npx degit https://github.com/lucia-auth/examples/tree/main/astro/github-oauth +``` + +## 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 + +Add a `github_id` and `username` column to your user table. + +| column | type | attributes | +| ----------- | -------- | ---------- | +| `github_id` | `number` | unique | +| `username` | `string` | | + +Create a `DatabaseUserAttributes` interface in the module declaration and add your database columns. By default, Lucia will not expose any database columns to the `User` type. To add a `githubId` and `username` field to it, use the `getUserAttributes()` option. + +```ts +import { Lucia } from "lucia"; + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: import.meta.env.PROD + } + }, + getUserAttributes: (attributes) => { + return { + // attributes has the type of DatabaseUserAttributes + githubId: attributes.github_id, + username: attributes.username + }; + } +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: DatabaseUserAttributes; + } +} + +interface DatabaseUserAttributes { + github_id: number; + username: string; +} +``` + +## Setup Arctic + +We recommend using [Arctic](https://arctic.js.org) for implementing OAuth. It is a lightweight library that provides APIs for creating authorization URLs, validating callbacks, and refreshing access tokens. This is the easiest way to implement OAuth with Lucia and it supports most major providers. + +``` +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 +); +``` + +## Sign in page + +Create `pages/login/index.astro` and add a basic sign in button, which should be a link to `/login/github`. + +```html + + + +

Sign in

+ Sign in with GitHub + + +``` + +## Create authorization URL + +Create an API route in `pages/login/github/index.ts`. Generate a new state, create a new authorization URL with createAuthorizationURL(), store the state, and redirect the user to the authorization URL. The user will be prompted to sign in with GitHub. + +```ts +// pages/login/github/index.ts +import { generateState } from "arctic"; +import { github } from "@lib/auth"; + +import type { APIContext } from "astro"; + +export async function GET(context: APIContext): Promise { + const state = generateState(); + const url = await github.createAuthorizationURL(state); + + context.cookies.set("github_oauth_state", state, { + path: "/", + secure: import.meta.env.PROD, + httpOnly: true, + maxAge: 60 * 10, + sameSite: "lax" + }); + + return context.redirect(url.toString()); +} +``` + +## Validate callback + +Create an API route in `pages/login/github/callback.ts` to handle the callback. First, get the state from the cookie and the search params and compare them. Validate the authorization code in the search params with `validateAuthorizationCode()`. This will throw an [`OAuth2RequestError`](https://oslo.js.org/reference/oauth2/OAuth2RequestError) if the code or credentials are invalid. After validating the code, get the user's profile using the access token. Check if the user is already registered with the GitHub ID, and create a new user if they aren't. Finally, create a new session and set the session cookie. + +```ts +// pages/login/github/callback.ts +import { github, lucia } from "@lib/auth"; +import { OAuth2RequestError } from "arctic"; +import { generateId } from "lucia"; + +import type { APIContext } from "astro"; + +export async function GET(context: APIContext): Promise { + 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 || !state || !storedState || state !== storedState) { + return new Response(null, { + status: 400 + }); + } + + try { + const tokens = await github.validateAuthorizationCode(code); + const githubUserResponse = await fetch("https://api.github.com/user", { + headers: { + Authorization: `Bearer ${tokens.accessToken}` + } + }); + const githubUser: GitHubUser = await githubUserResponse.json(); + const existingUser = await db.table("user").where("github_id", "=", githubUser.id).get(); + + if (existingUser) { + const session = await lucia.createSession(existingUser.id, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + context.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + return context.redirect("/"); + } + + const userId = generateId(15); + await db.table("user").insert({ + id: userId, + github_id: githubUser.id, + username: githubUser.login + }); + const session = await lucia.createSession(userId, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + context.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + return context.redirect("/"); + } catch (e) { + // the specific error message depends on the provider + if (e instanceof OAuth2RequestError) { + // invalid code + return new Response(null, { + status: 400 + }); + } + return new Response(null, { + status: 500 + }); + } +} + +interface GitHubUser { + id: string; + login: string; +} +``` + +## Validate requests + +You can validate requests by checking `locals.user`. The field `user.username` is available since we defined the `getUserAttributes()` option. You can protect pages, such as `/`, by redirecting unauthenticated users to the login page. + +```ts +const user = Astro.locals.user; +if (!user) { + return Astro.redirect("/login"); +} + +const username = user.username; +``` + +## Sign out + +Sign out users by invalidating their session with `Lucia.invalidateSession()`. Make sure to remove their session cookie by setting a blank session cookie created with `Lucia.createBlankSessionCookie()`. + +```ts +import { lucia } from "@lib/auth"; +import type { APIContext } from "astro"; + +export async function POST(context: APIContext): Promise { + if (!context.locals.session) { + return new Response(null, { + status: 401 + }); + } + + await lucia.invalidateSession(context.locals.session.id); + + const sessionCookie = lucia.createBlankSessionCookie(); + context.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + + return Astro.redirect("/login"); +} +``` + +```html +
+ +
+``` diff --git a/docs/pages/translations/ja/tutorials/github-oauth/index.md b/docs/pages/translations/ja/tutorials/github-oauth/index.md new file mode 100644 index 000000000..331cb73db --- /dev/null +++ b/docs/pages/translations/ja/tutorials/github-oauth/index.md @@ -0,0 +1,13 @@ +--- +title: "Tutorial: GitHub OAuth" +--- + +# Tutorial: GitHub OAuth + +The tutorials go over how to implement a basic GitHub OAuth and cover the basics of Lucia along the way. As a prerequisite, you should be fairly comfortable with your framework and its APIs. Basic example projects are available in the [examples repository](https://github.com/lucia-auth/examples/tree/main). + +- [Astro](/tutorials/github-oauth/astro) +- [Next.js App router](/tutorials/github-oauth/nextjs-app) +- [Next.js Pages router](/tutorials/github-oauth/nextjs-pages) +- [Nuxt](/tutorials/github-oauth/nuxt) +- [SvelteKit](/tutorials/github-oauth/sveltekit) diff --git a/docs/pages/translations/ja/tutorials/github-oauth/nextjs-app.md b/docs/pages/translations/ja/tutorials/github-oauth/nextjs-app.md new file mode 100644 index 000000000..7cecf4b8c --- /dev/null +++ b/docs/pages/translations/ja/tutorials/github-oauth/nextjs-app.md @@ -0,0 +1,293 @@ +--- +title: "GitHub OAuth in Next.js App router" +--- + +# Tutorial: GitHub OAuth in Next.js App router + +Before starting, make sure you've set up your database and middleware as described in the [Getting started](/getting-started/nextjs-app) page. + +An [example project](https://github.com/lucia-auth/examples/tree/main/nextjs-app/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/examples/tree/v3/nextjs-app/github-oauth). + +``` +npx degit https://github.com/lucia-auth/examples/tree/main/nextjs-app/github-oauth +``` + +## 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 + +Add a `github_id` and `username` column to your user table. + +| column | type | attributes | +| ----------- | -------- | ---------- | +| `github_id` | `number` | unique | +| `username` | `string` | | + +Create a `DatabaseUserAttributes` interface in the module declaration and add your database columns. By default, Lucia will not expose any database columns to the `User` type. To add a `githubId` and `username` field to it, use the `getUserAttributes()` option. + +```ts +import { Lucia } from "lucia"; + +export const lucia = new Lucia(adapter, { + sessionCookie: { + expires: false, + attributes: { + secure: process.env.NODE_ENV === "production" + } + }, + getUserAttributes: (attributes) => { + return { + // attributes has the type of DatabaseUserAttributes + githubId: attributes.github_id, + username: attributes.username + }; + } +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: DatabaseUserAttributes; + } +} + +interface DatabaseUserAttributes { + github_id: number; + username: string; +} +``` + +## Setup Arctic + +We recommend using [Arctic](https://arctic.js.org) for implementing OAuth. It is a lightweight library that provides APIs for creating authorization URLs, validating callbacks, and refreshing access tokens. This is the easiest way to implement OAuth with Lucia and it supports most major providers. + +``` +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!); +``` + +## 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 ( + <> +

Sign in

+ Sign in with GitHub + + ); +} +``` + +## Create authorization URL + +Create an API route in `app/login/github/route.ts`. Generate a new state, create a new authorization URL with createAuthorizationURL(), store the state, and redirect the user to the authorization URL. The user will be prompted to sign in with GitHub. + +```ts +// app/login/github/route.ts +import { generateState } from "arctic"; +import { github } from "../../../lib/auth"; +import { cookies } from "next/headers"; + +export async function GET(): Promise { + const state = generateState(); + const url = await github.createAuthorizationURL(state); + + cookies().set("github_oauth_state", state, { + path: "/", + secure: process.env.NODE_ENV === "production", + httpOnly: true, + maxAge: 60 * 10, + sameSite: "lax" + }); + + return Response.redirect(url); +} +``` + +## Validate callback + +Create an API route in `app/login/github/callback/route.ts` to handle the callback. First, get the state from the cookie and the search params and compare them. Validate the authorization code in the search params with `validateAuthorizationCode()`. This will throw an [`OAuth2RequestError`](https://oslo.js.org/reference/oauth2/OAuth2RequestError) if the code or credentials are invalid. After validating the code, get the user's profile using the access token. Check if the user is already registered with the GitHub ID, and create a new user if they aren't. Finally, create a new session and set the session cookie. + +```ts +// app/login/github/callback/route.ts +import { github, lucia } from "@/lib/auth"; +import { cookies } from "next/headers"; +import { OAuth2RequestError } from "arctic"; +import { generateId } from "lucia"; + +export async function GET(request: Request): Promise { + const url = new URL(request.url); + const code = url.searchParams.get("code"); + const state = url.searchParams.get("state"); + const storedState = cookies().get("github_oauth_state")?.value ?? null; + if (!code || !state || !storedState || state !== storedState) { + return new Response(null, { + status: 400 + }); + } + + try { + const tokens = await github.validateAuthorizationCode(code); + const githubUserResponse = await fetch("https://api.github.com/user", { + headers: { + Authorization: `Bearer ${tokens.accessToken}` + } + }); + const githubUser: GitHubUser = await githubUserResponse.json(); + const existingUser = await db.table("user").where("github_id", "=", githubUser.id).get(); + + if (existingUser) { + const session = await lucia.createSession(existingUser.id, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + return new Response(null, { + status: 302, + headers: { + Location: "/" + } + }); + } + + const userId = generateId(15); + await db.table("user").insert({ + id: userId, + github_id: githubUser.id, + username: githubUser.login + }); + const session = await lucia.createSession(userId, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + return new Response(null, { + status: 302, + headers: { + Location: "/" + } + }); + } catch (e) { + // the specific error message depends on the provider + if (e instanceof OAuth2RequestError) { + // invalid code + return new Response(null, { + status: 400 + }); + } + return new Response(null, { + status: 500 + }); + } +} + +interface GitHubUser { + id: string; + login: string; +} +``` + +## Validate requests + +Create `validateRequest()`. This will check for the session cookie, validate it, and set a new cookie if necessary. Make sure to catch errors when setting cookies and wrap the function with `cache()` to prevent unnecessary database calls. To learn more, see the [Validating requests](/basics/validate-session-cookies/nextjs-app) page. + +CSRF protection should be implemented but Next.js handles it when using form actions (but not for API routes). + +```ts +import { cookies } from "next/headers"; +import { cache } from "react"; + +import type { Session, User } from "lucia"; + +export const lucia = new Lucia(); + +export const validateRequest = cache( + async (): Promise<{ user: User; session: Session } | { user: null; session: null }> => { + const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null; + if (!sessionId) { + return { + user: null, + session: null + }; + } + + const result = await lucia.validateSession(sessionId); + // next.js throws when you attempt to set cookie when rendering page + try { + if (result.session && result.session.fresh) { + const sessionCookie = lucia.createSessionCookie(result.session.id); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + } + if (!result.session) { + const sessionCookie = lucia.createBlankSessionCookie(); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + } + } catch {} + return result; + } +); +``` + +This function can then be used in server components and form actions to get the current session and user. + +```tsx +import { redirect } from "next/navigation"; +import { validateRequest } from "@/lib/auth"; + +export default async function Page() { + const { user } = await validateRequest(); + if (!user) { + return redirect("/login"); + } + return

Hi, {user.username}!

; +} +``` + +## Sign out + +Sign out users by invalidating their session with `Lucia.invalidateSession()`. Make sure to remove their session cookie by setting a blank session cookie created with `Lucia.createBlankSessionCookie()`. + +```tsx +import { lucia, validateRequest } from "@/lib/auth"; +import { redirect } from "next/navigation"; +import { cookies } from "next/headers"; + +export default async function Page() { + return ( +
+ +
+ ); +} + +async function logout(): Promise { + "use server"; + const { session } = await validateRequest(); + if (!session) { + return { + error: "Unauthorized" + }; + } + + await lucia.invalidateSession(session.id); + + const sessionCookie = lucia.createBlankSessionCookie(); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + return redirect("/login"); +} +``` diff --git a/docs/pages/translations/ja/tutorials/github-oauth/nextjs-pages.md b/docs/pages/translations/ja/tutorials/github-oauth/nextjs-pages.md new file mode 100644 index 000000000..550dcce79 --- /dev/null +++ b/docs/pages/translations/ja/tutorials/github-oauth/nextjs-pages.md @@ -0,0 +1,324 @@ +--- +title: "GitHub OAuth in Next.js Pages router" +--- + +# Tutorial: GitHub OAuth in Next.js Pages router + +Before starting, make sure you've set up your database and middleware as described in the [Getting started](/getting-started/nextjs-pages) page. + +An [example project](https://github.com/lucia-auth/examples/tree/main/nextjs-pages/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/examples/tree/v3/nextjs-pages/github-oauth). + +``` +npx degit https://github.com/lucia-auth/examples/tree/main/nextjs-pages/github-oauth +``` + +## 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/api/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 + +Add a `github_id` and `username` column to your user table. + +| column | type | attributes | +| ----------- | -------- | ---------- | +| `github_id` | `number` | unique | +| `username` | `string` | | + +Create a `DatabaseUserAttributes` interface in the module declaration and add your database columns. By default, Lucia will not expose any database columns to the `User` type. To add a `githubId` and `username` field to it, use the `getUserAttributes()` option. + +```ts +import { Lucia } from "lucia"; + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: process.env.NODE_ENV === "production" + } + }, + getUserAttributes: (attributes) => { + return { + // attributes has the type of DatabaseUserAttributes + githubId: attributes.github_id, + username: attributes.username + }; + } +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: DatabaseUserAttributes; + } +} + +interface DatabaseUserAttributes { + github_id: number; + username: string; +} +``` + +## Setup Arctic + +We recommend using [Arctic](https://arctic.js.org) for implementing OAuth. It is a lightweight library that provides APIs for creating authorization URLs, validating callbacks, and refreshing access tokens. This is the easiest way to implement OAuth with Lucia and it supports most major providers. + +``` +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!); +``` + +## Sign in page + +Create `pages/login.tsx` and add a basic sign in button, which should be a link to `/login/github`. + +```tsx +// pages/login.tsx +export default function Page() { + return ( + <> +

Sign in

+ Sign in with GitHub + + ); +} +``` + +## Create authorization URL + +Create an API route in `pages/api/login/github/index.ts`. Generate a new state, create a new authorization URL with createAuthorizationURL(), store the state, and redirect the user to the authorization URL. The user will be prompted to sign in with GitHub. + +```ts +// pages/api/login/github/index.ts +import { github } from "@/lib/auth"; +import { generateState } from "arctic"; +import { serializeCookie } from "oslo/cookie"; + +import type { NextApiRequest, NextApiResponse } from "next"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "GET") { + res.status(404).end(); + return; + } + const state = generateState(); + const url = await github.createAuthorizationURL(state); + res + .appendHeader( + "Set-Cookie", + serializeCookie("github_oauth_state", state, { + path: "/", + secure: process.env.NODE_ENV === "production", + httpOnly: true, + maxAge: 60 * 10, + sameSite: "lax" + }) + ) + .redirect(url.toString()); +} +``` + +## Validate callback + +Create an API route in `pages/api/login/github/callback.ts` to handle the callback. First, get the state from the cookie and the search params and compare them. Validate the authorization code in the search params with `validateAuthorizationCode()`. This will throw an [`OAuth2RequestError`](https://oslo.js.org/reference/oauth2/OAuth2RequestError) if the code or credentials are invalid. After validating the code, get the user's profile using the access token. Check if the user is already registered with the GitHub ID, and create a new user if they aren't. Finally, create a new session and set the session cookie. + +```ts +// pages/api/login/github/callback.ts +import { github, lucia } from "@/lib/auth"; +import { OAuth2RequestError } from "arctic"; +import { generateId } from "lucia"; + +import type { NextApiRequest, NextApiResponse } from "next"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "GET") { + res.status(404).end(); + return; + } + const code = req.query.code?.toString() ?? null; + const state = req.query.state?.toString() ?? null; + const storedState = req.cookies.github_oauth_state ?? null; + if (!code || !state || !storedState || state !== storedState) { + console.log(code, state, storedState); + res.status(400).end(); + return; + } + try { + const tokens = await github.validateAuthorizationCode(code); + const githubUserResponse = await fetch("https://api.github.com/user", { + headers: { + Authorization: `Bearer ${tokens.accessToken}` + } + }); + const githubUser: GitHubUser = await githubUserResponse.json(); + const existingUser = await db.table("user").where("github_id", "=", githubUser.id).get(); + + if (existingUser) { + const session = await lucia.createSession(existingUser.id, {}); + return res + .appendHeader("Set-Cookie", lucia.createSessionCookie(session.id).serialize()) + .redirect("/"); + } + + const userId = generateId(15); + await db.table("user").insert({ + id: userId, + github_id: githubUser.id, + username: githubUser.login + }); + const session = await lucia.createSession(userId, {}); + return res + .appendHeader("Set-Cookie", lucia.createSessionCookie(session.id).serialize()) + .redirect("/"); + } catch (e) { + // the specific error message depends on the provider + if (e instanceof OAuth2RequestError) { + // invalid code + return new Response(null, { + status: 400 + }); + } + res.status(500).end(); + return; + } +} + +interface GitHubUser { + id: string; + login: string; +} +``` + +## Validate requests + +Create `validateRequest()`. This will check for the session cookie, validate it, and set a new cookie if necessary. To learn more, see the [Validating requests](/basics/validate-session-cookies/nextjs-pages) page. + +CSRF protection should be implemented and you should already have a middleware for it. + +```ts +import type { Session, User } from "lucia"; +import type { IncomingMessage, ServerResponse } from "http"; + +export const lucia = new Lucia(); + +export async function validateRequest( + req: IncomingMessage, + res: ServerResponse +): Promise<{ user: User; session: Session } | { user: null; session: null }> { + const sessionId = lucia.readSessionCookie(req.headers.cookie ?? ""); + if (!sessionId) { + return { + user: null, + session: null + }; + } + const result = await lucia.validateSession(sessionId); + if (result.session && result.session.fresh) { + res.appendHeader("Set-Cookie", lucia.createSessionCookie(result.session.id).serialize()); + } + if (!result.session) { + res.appendHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize()); + } + return result; +} +``` + +This function can then be used in both `getServerSideProps()` and API routes. + +```tsx +import { validateRequest } from "@/lib/auth"; + +import type { + GetServerSidePropsContext, + GetServerSidePropsResult, + InferGetServerSidePropsType +} from "next"; +import type { User } from "lucia"; + +export async function getServerSideProps(context: GetServerSidePropsContext): Promise< + GetServerSidePropsResult<{ + user: User; + }> +> { + const { user } = await validateRequest(context.req, context.res); + if (!user) { + return { + redirect: { + permanent: false, + destination: "/login" + } + }; + } + return { + props: { + user + } + }; +} + +export default function Page({ user }: InferGetServerSidePropsType) { + return

Hi, {user.username}!

; +} +``` + +## Sign out + +Sign out users by invalidating their session with `Lucia.invalidateSession()`. Make sure to remove their session cookie by setting a blank session cookie created with `Lucia.createBlankSessionCookie()`. + +```ts +// pages/api/logout.ts +import { lucia, validateRequest } from "@/lib/auth"; + +import type { NextApiRequest, NextApiResponse } from "next"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "POST") { + res.status(404).end(); + return; + } + const { session } = await validateRequest(req, res); + if (!session) { + res.status(401).end(); + return; + } + await lucia.invalidateSession(session.id); + res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize()).status(200).end(); +} +``` + +```tsx +import { useRouter } from "next/router"; + +import type { FormEvent } from "react"; + +export default function Page({ user }: InferGetServerSidePropsType) { + const router = useRouter(); + + async function onSubmit(e: FormEvent) { + e.preventDefault(); + const formElement = e.target as HTMLFormElement; + await fetch(formElement.action, { + method: formElement.method + }); + router.push("/login"); + } + + return ( +
+ +
+ ); +} +``` diff --git a/docs/pages/translations/ja/tutorials/github-oauth/nuxt.md b/docs/pages/translations/ja/tutorials/github-oauth/nuxt.md new file mode 100644 index 000000000..684c7e532 --- /dev/null +++ b/docs/pages/translations/ja/tutorials/github-oauth/nuxt.md @@ -0,0 +1,270 @@ +--- +title: "GitHub OAuth in Nuxt" +--- + +# Tutorial: GitHub OAuth in Nuxt + +Before starting, make sure you've set up your database and middleware as described in the [Getting started](/getting-started/nuxt) page. + +An [example project](https://github.com/lucia-auth/examples/tree/main/nuxt/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/examples/tree/v3/nuxt/github-oauth). + +``` +npx degit https://github.com/lucia-auth/examples/tree/main/nuxt/github-oauth +``` + +## 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 + +Add a `github_id` and `username` column to your user table. + +| column | type | attributes | +| ----------- | -------- | ---------- | +| `github_id` | `number` | unique | +| `username` | `string` | | + +Create a `DatabaseUserAttributes` interface in the module declaration and add your database columns. By default, Lucia will not expose any database columns to the `User` type. To add a `githubId` and `username` field to it, use the `getUserAttributes()` option. + +```ts +// server/utils/auth.ts +import { Lucia } from "lucia"; + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: !import.meta.dev + } + }, + getUserAttributes: (attributes) => { + return { + // attributes has the type of DatabaseUserAttributes + githubId: attributes.github_id, + username: attributes.username + }; + } +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: DatabaseUserAttributes; + } +} + +interface DatabaseUserAttributes { + github_id: number; + username: string; +} +``` + +## Setup Arctic + +We recommend using [Arctic](https://arctic.js.org) for implementing OAuth. It is a lightweight library that provides APIs for creating authorization URLs, validating callbacks, and refreshing access tokens. This is the easiest way to implement OAuth with Lucia and it supports most major providers. + +``` +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!); +``` + +## Sign in page + +Create `pages/login/index.vue` and add a basic sign in button, which should be a link to `/login/github`. + +```vue + + +``` + +## Create authorization URL + +Create an API route in `server/routes/login/github/index.get.ts`. Generate a new state, create a new authorization URL with createAuthorizationURL(), store the state, and redirect the user to the authorization URL. The user will be prompted to sign in with GitHub. + +```ts +// server/routes/login/github/index.get.ts +import { generateState } from "arctic"; + +export default defineEventHandler(async (event) => { + const state = generateState(); + const url = await github.createAuthorizationURL(state); + + setCookie(event, "github_oauth_state", state, { + path: "/", + secure: process.env.NODE_ENV === "production", + httpOnly: true, + maxAge: 60 * 10, + sameSite: "lax" + }); + return sendRedirect(event, url.toString()); +}); +``` + +## Validate callback + +Create an API route in `server/routes/login/github/callback.get.ts` to handle the callback. First, get the state from the cookie and the search params and compare them. Validate the authorization code in the search params with `validateAuthorizationCode()`. This will throw an [`OAuth2RequestError`](https://oslo.js.org/reference/oauth2/OAuth2RequestError) if the code or credentials are invalid. After validating the code, get the user's profile using the access token. Check if the user is already registered with the GitHub ID, and create a new user if they aren't. Finally, create a new session and set the session cookie. + +```ts +// server/routes/login/github/callback.get.ts +import { OAuth2RequestError } from "arctic"; +import { generateId } from "lucia"; + +export default defineEventHandler(async (event) => { + const query = getQuery(event); + const code = query.code?.toString() ?? null; + const state = query.state?.toString() ?? null; + const storedState = getCookie(event, "github_oauth_state") ?? null; + if (!code || !state || !storedState || state !== storedState) { + throw createError({ + status: 400 + }); + } + + try { + const tokens = await github.validateAuthorizationCode(code); + const githubUserResponse = await fetch("https://api.github.com/user", { + headers: { + Authorization: `Bearer ${tokens.accessToken}` + } + }); + const githubUser: GitHubUser = await githubUserResponse.json(); + const existingUser = await db.table("user").where("github_id", "=", githubUser.id).get(); + + if (existingUser) { + const session = await lucia.createSession(existingUser.id, {}); + appendHeader(event, "Set-Cookie", lucia.createSessionCookie(session.id).serialize()); + return sendRedirect(event, "/"); + } + + const userId = generateId(15); + await db.table("user").insert({ + id: userId, + github_id: githubUser.id, + username: githubUser.login + }); + const session = await lucia.createSession(userId, {}); + appendHeader(event, "Set-Cookie", lucia.createSessionCookie(session.id).serialize()); + return sendRedirect(event, "/"); + } catch (e) { + // the specific error message depends on the provider + if (e instanceof OAuth2RequestError) { + // invalid code + throw createError({ + status: 400 + }); + } + throw createError({ + status: 500 + }); + } +}); + +interface GitHubUser { + id: string; + login: string; +} +``` + +## Validate requests + +You can validate requests by checking `event.context.user`. The field `user.username` is available since we defined the `getUserAttributes()` option. You can protect pages, such as `/`, by redirecting unauthenticated users to the login page. + +```ts +export default defineEventHandler((event) => { + if (event.context.user) { + const username = event.context.user.username; + } + // ... +}); +``` + +## Get user in the client + +Create an API route in `server/api/user.get.ts`. This will just return the current user. + +```ts +// server/api/user.get.ts +export default defineEventHandler((event) => { + return event.context.user; +}); +``` + +Create a composable `useUser()` in `composables/auth.ts`. + +```ts +// composables/auth.ts +import type { User } from "lucia"; + +export const useUser = () => { + const user = useState("user", () => null); + return user; +}; +``` + +Then, create a global middleware in `middleware/auth.global.ts` to populate it. + +```ts +// middleware/auth.global.ts +export default defineNuxtRouteMiddleware(async () => { + const user = useUser(); + user.value = await $fetch("/api/user"); +}); +``` + +You can now use `useUser()` client side to get the current user. + +```vue + +``` + +## Sign out + +Sign out users by invalidating their session with `Lucia.invalidateSession()`. Make sure to remove their session cookie by setting a blank session cookie created with `Lucia.createBlankSessionCookie()`. + +```ts +// server/api/logout.post.ts +export default eventHandler(async (event) => { + if (!event.context.session) { + throw createError({ + statusCode: 403 + }); + } + await lucia.invalidateSession(event.context.session.id); + appendHeader(event, "Set-Cookie", lucia.createBlankSessionCookie().serialize()); +}); +``` + +```vue + + + +``` diff --git a/docs/pages/translations/ja/tutorials/github-oauth/sveltekit.md b/docs/pages/translations/ja/tutorials/github-oauth/sveltekit.md new file mode 100644 index 000000000..d86c55cc7 --- /dev/null +++ b/docs/pages/translations/ja/tutorials/github-oauth/sveltekit.md @@ -0,0 +1,260 @@ +--- +title: "GitHub OAuth in SvelteKit" +--- + +# Tutorial: GitHub OAuth in SvelteKit + +Before starting, make sure you've set up your database and middleware as described in the [Getting started](/getting-started/sveltekit) page. + +An [example project](https://github.com/lucia-auth/examples/tree/main/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/examples/tree/v3/sveltekit/github-oauth). + +``` +npx degit https://github.com/lucia-auth/examples/tree/main/sveltekit/github-oauth +``` + +## 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 + +Add a `github_id` and `username` column to your user table. + +| column | type | attributes | +| ----------- | -------- | ---------- | +| `github_id` | `number` | unique | +| `username` | `string` | | + +Create a `DatabaseUserAttributes` interface in the module declaration and add your database columns. By default, Lucia will not expose any database columns to the `User` type. To add a `githubId` and `username` field to it, use the `getUserAttributes()` option. + +```ts +import { Lucia } from "lucia"; +import { dev } from "$app/environment"; + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: !dev + } + }, + getUserAttributes: (attributes) => { + return { + // attributes has the type of DatabaseUserAttributes + githubId: attributes.github_id, + username: attributes.username + }; + } +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: DatabaseUserAttributes; + } +} + +interface DatabaseUserAttributes { + github_id: number; + username: string; +} +``` + +## Setup Arctic + +We recommend using [Arctic](https://arctic.js.org) for implementing OAuth. It is a lightweight library that provides APIs for creating authorization URLs, validating callbacks, and refreshing access tokens. This is the easiest way to implement OAuth with Lucia and it supports most major providers. + +``` +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 +); +``` + +## Sign in page + +Create `routes/login/+page.svelte` and add a basic sign in button, which should be a link to `/login/github`. + +```svelte + +

Sign in

+Sign in with GitHub +``` + +## Create authorization URL + +Create an API route in `routes/login/github/+server.ts`. Generate a new state, create a new authorization URL with createAuthorizationURL(), store the state, and redirect the user to the authorization URL. The user will be prompted to sign in with GitHub. + +```ts +// routes/login/github/+server.ts +import { github } from "$lib/server/auth"; +import { generateState } from "arctic"; +import { redirect } from "@sveltejs/kit"; + +import type { RequestEvent } from "@sveltejs/kit"; + +export async function GET(event: RequestEvent): Promise { + const state = generateState(); + const url = await github.createAuthorizationURL(state); + + event.cookies.set("github_oauth_state", state, { + path: "/", + secure: import.meta.env.PROD, + httpOnly: true, + maxAge: 60 * 10, + sameSite: "lax" + }); + + redirect(302, url.toString()); +} +``` + +## Validate callback + +Create an API route in `routes/login/github/callback/+server.ts` to handle the callback. First, get the state from the cookie and the search params and compare them. Validate the authorization code in the search params with `validateAuthorizationCode()`. This will throw an [`OAuth2RequestError`](https://oslo.js.org/reference/oauth2/OAuth2RequestError) if the code or credentials are invalid. After validating the code, get the user's profile using the access token. Check if the user is already registered with the GitHub ID, and create a new user if they aren't. Finally, create a new session and set the session cookie. + +```ts +// routes/login/github/callback/+server.ts +import { github, lucia } from "$lib/server/auth"; +import { OAuth2RequestError } from "arctic"; +import { generateId } from "lucia"; + +import type { RequestEvent } from "@sveltejs/kit"; + +export async function GET(event: RequestEvent): Promise { + 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 || !state || !storedState || state !== storedState) { + return new Response(null, { + status: 400 + }); + } + + try { + const tokens = await github.validateAuthorizationCode(code); + const githubUserResponse = await fetch("https://api.github.com/user", { + headers: { + Authorization: `Bearer ${tokens.accessToken}` + } + }); + const githubUser: GitHubUser = await githubUserResponse.json(); + const existingUser = await db.table("user").where("github_id", "=", githubUser.id).get(); + + if (existingUser) { + const session = await lucia.createSession(existingUser.id, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + event.cookies.set(sessionCookie.name, sessionCookie.value, { + path: ".", + ...sessionCookie.attributes + }); + } else { + const userId = generateId(15); + await db.table("user").insert({ + id: userId, + github_id: githubUser.id, + username: githubUser.login + }); + const session = await lucia.createSession(userId, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + event.cookies.set(sessionCookie.name, sessionCookie.value, { + path: ".", + ...sessionCookie.attributes + }); + } + return new Response(null, { + status: 302, + headers: { + Location: "/" + } + }); + } catch (e) { + // the specific error message depends on the provider + if (e instanceof OAuth2RequestError) { + // invalid code + return new Response(null, { + status: 400 + }); + } + return new Response(null, { + status: 500 + }); + } +} + +interface GitHubUser { + id: string; + login: string; +} +``` + +## Validate requests + +You can validate requests by checking `locals.user`. The field `user.username` is available since we defined the `getUserAttributes()` option. You can protect pages, such as `/`, by redirecting unauthenticated users to the login page. + +```ts +// +page.server.ts +import type { PageServerLoad, Actions } from "./$types"; + +export const load: PageServerLoad = async (event) => { + if (!event.locals.user) redirect(302, "/login"); + return { + username: event.locals.user.username + }; +}; +``` + +## Sign out user + +Sign out users by invalidating their session with `Lucia.invalidateSession()`. Make sure to remove their session cookie by setting a blank session cookie created with `Lucia.createBlankSessionCookie()`. + +```ts +// routes/+page.server.ts +import { lucia } from "$lib/server/auth"; +import { fail, redirect } from "@sveltejs/kit"; + +import type { Actions, PageServerLoad } from "./$types"; + +export const load: PageServerLoad = async ({ locals }) => { + // ... +}; + +export const actions: Actions = { + default: async (event) => { + if (!event.locals.session) { + return fail(401); + } + await auth.invalidateSession(event.locals.session.id); + const sessionCookie = lucia.createBlankSessionCookie(); + context.cookies.set(sessionCookie.name, sessionCookie.value, { + path: ".", + ...sessionCookie.attributes + }); + redirect(302, "/login"); + } +}; +``` + +```svelte + + + +
+ +
+``` diff --git a/docs/pages/translations/ja/tutorials/index.md b/docs/pages/translations/ja/tutorials/index.md new file mode 100644 index 000000000..108e4c415 --- /dev/null +++ b/docs/pages/translations/ja/tutorials/index.md @@ -0,0 +1,25 @@ +--- +title: "Tutorials" +--- + +# Lucia Auth Tutorials + +Explore our tutorials for implementing Lucia Auth with GitHub OAuth or traditional username and password authentication in Astro, SvelteKit, Nuxt, and Next.js. + +## GitHub OAuth + +Learn to set up GitHub OAuth, handle authentication, and manage user sessions. + +- [Astro](/tutorials/github-oauth/astro) +- [SvelteKit](/tutorials/github-oauth/sveltekit) +- [Nuxt](/tutorials/github-oauth/nuxt) +- [Next.js](/tutorials/github-oauth/nextjs) + +## Username and Password + +Understand how to implement a secure username and password authentication system. + +- [Astro](/tutorials/username-and-password/astro) +- [SvelteKit](/tutorials/username-and-password/sveltekit) +- [Nuxt](/tutorials/username-and-password/nuxt) +- [Next.js](/tutorials/username-and-password/nextjs) diff --git a/docs/pages/translations/ja/tutorials/username-and-password/astro.md b/docs/pages/translations/ja/tutorials/username-and-password/astro.md new file mode 100644 index 000000000..4994cccec --- /dev/null +++ b/docs/pages/translations/ja/tutorials/username-and-password/astro.md @@ -0,0 +1,263 @@ +--- +title: "Tutorial: Username and password auth in Astro" +--- + +# Tutorial: Username and password auth in Astro + +Before starting, make sure you've set up your database and middleware as described in the [Getting started](/getting-started/astro) page. + +An [example project](https://github.com/lucia-auth/examples/tree/main/astro/username-and-password) based on this tutorial is also available. You can clone the example locally or [open it in StackBlitz](https://stackblitz.com/github/lucia-auth/examples/tree/v3/astro/username-and-password). + +``` +npx degit https://github.com/lucia-auth/examples/tree/main/astro/username-and-password +``` + +## Update database + +Add a `username` and `hashed_password` column to your user table. + +| column | type | attributes | +| ----------------- | -------- | ---------- | +| `username` | `string` | unique | +| `hashed_password` | `string` | | + +Create a `DatabaseUserAttributes` interface in the module declaration and add your database columns. By default, Lucia will not expose any database columns to the `User` type. To add a `username` field to it, use the `getUserAttributes()` option. + +```ts +import { Lucia } from "lucia"; + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: import.meta.env.PROD + } + }, + getUserAttributes: (attributes) => { + return { + // attributes has the type of DatabaseUserAttributes + username: attributes.username + }; + } +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: DatabaseUserAttributes; + } +} + +interface DatabaseUserAttributes { + username: string; +} +``` + +## Sign up user + +Create `pages/signup.astro` and set up a basic form. + +```html + + + +

Sign up

+
+ + + + + +
+ + +``` + +Create an API route in `pages/api/signup.ts`. First, do a very basic input validation. Hash the password, generate a new user ID, and create a new user. If successful, create a new session with `Lucia.createSession()` and set a new session cookie. + +```ts +// pages/api/signup.ts +import { lucia } from "@lib/auth"; +import { generateId } from "lucia"; +import { Argon2id } from "oslo/password"; + +import type { APIContext } from "astro"; + +export async function POST(context: APIContext): Promise { + const formData = await context.request.formData(); + const username = formData.get("username"); + // username must be between 4 ~ 31 characters, and only consists of lowercase letters, 0-9, -, and _ + // keep in mind some database (e.g. mysql) are case insensitive + if ( + typeof username !== "string" || + username.length < 3 || + username.length > 31 || + !/^[a-z0-9_-]+$/.test(username) + ) { + return new Response("Invalid username", { + status: 400 + }); + } + const password = formData.get("password"); + if (typeof password !== "string" || password.length < 6 || password.length > 255) { + return new Response("Invalid password", { + status: 400 + }); + } + + const userId = generateId(15); + const hashedPassword = await new Argon2id().hash(password); + + // TODO: check if username is already used + await db.table("user").insert({ + id: userId, + username: username, + hashed_password: hashedPassword + }); + + const session = await lucia.createSession(userId, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + context.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + + return context.redirect("/"); +} +``` + +We recommend using Argon2id, but Oslo also provides Scrypt and Bcrypt. These only work in Node.js. If you're planning to deploy your project to a non-Node.js runtime, use `Scrypt` provided by `lucia`. This is a pure JS implementation but 2~3 times slower. For Bun, use [`Bun.password`](https://bun.sh/docs/api/hashing#bun-password). + +```ts +import { Scrypt } from "lucia"; + +new Scrypt().hash(password); +``` + +**If you're using Bcrypt, [set the maximum password length to 64 _bytes_](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#input-limits-of-bcrypt).** + +```ts +const length = new TextEncoder().encode(password).length; +``` + +## Sign in user + +Create `pages/login.astro` and set up a basic form. + +```html + + + +

Sign in

+
+ + + + + +
+ + +``` + +Create an API route as `pages/api/signup.ts`. First, do a very basic input validation. Get the user with the username and verify the password. If successful, create a new session with `Lucia.createSession()` and set a new session cookie. + +```ts +// pages/api/login.ts +import { lucia } from "@lib/auth"; +import { Argon2id } from "oslo/password"; + +import type { APIContext } from "astro"; + +export async function POST(context: APIContext): Promise { + const formData = await context.request.formData(); + const username = formData.get("username"); + if ( + typeof username !== "string" || + username.length < 3 || + username.length > 31 || + !/^[a-z0-9_-]+$/.test(username) + ) { + return new Response("Invalid username", { + status: 400 + }); + } + const password = formData.get("password"); + if (typeof password !== "string" || password.length < 6 || password.length > 255) { + return new Response("Invalid password", { + status: 400 + }); + } + + const existingUser = await db + .table("username") + .where("username", "=", username.toLowerCase()) + .get(); + if (!existingUser) { + // NOTE: + // Returning immediately allows malicious actors to figure out valid usernames from response times, + // allowing them to only focus on guessing passwords in brute-force attacks. + // As a preventive measure, you may want to hash passwords even for invalid usernames. + // However, valid usernames can be already be revealed with the signup page among other methods. + // It will also be much more resource intensive. + // Since protecting against this is none-trivial, + // it is crucial your implementation is protected against brute-force attacks with login throttling etc. + // If usernames are public, you may outright tell the user that the username is invalid. + return new Response("Incorrect username or password", { + status: 400 + }); + } + + const validPassword = await new Argon2id().verify(existingUser.password, password); + if (!validPassword) { + return new Response("Incorrect username or password", { + status: 400 + }); + } + + const session = await lucia.createSession(userId, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + context.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + + return context.redirect("/"); +} +``` + +## Validate requests + +You can validate requests by checking `locals.user`. The field `user.username` is available since we defined the `getUserAttributes()` option. You can protect pages, such as `/`, by redirecting unauthenticated users to the login page. + +```ts +const user = Astro.locals.user; +if (!user) { + return Astro.redirect("/login"); +} + +const username = user.username; +``` + +## Sign out + +Sign out users by invalidating their session with `Lucia.invalidateSession()`. Make sure to remove their session cookie by setting a blank session cookie created with `Lucia.createBlankSessionCookie()`. + +```ts +import { lucia } from "@lib/auth"; +import type { APIContext } from "astro"; + +export async function POST(context: APIContext): Promise { + if (!context.locals.session) { + return new Response(null, { + status: 401 + }); + } + + await lucia.invalidateSession(context.locals.session.id); + + const sessionCookie = lucia.createBlankSessionCookie(); + context.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + + return Astro.redirect("/login"); +} +``` + +```html +
+ +
+``` diff --git a/docs/pages/translations/ja/tutorials/username-and-password/index.md b/docs/pages/translations/ja/tutorials/username-and-password/index.md new file mode 100644 index 000000000..60b26e151 --- /dev/null +++ b/docs/pages/translations/ja/tutorials/username-and-password/index.md @@ -0,0 +1,13 @@ +--- +title: "Tutorial: Username and password" +--- + +# Tutorial: Username and password auth + +The tutorials go over how to implement a basic username and password auth and cover the basics of Lucia along the way. As a prerequisite, you should be fairly comfortable with your framework and its APIs. For a more in-depth guide, see the [Email and password](/guides/email-and-password/) guides. Basic example projects are available in the [examples repository](https://github.com/lucia-auth/examples). + +- [Astro](/tutorials/username-and-password/astro) +- [Next.js App router](/tutorials/username-and-password/nextjs-app) +- [Next.js Pages router](/tutorials/username-and-password/nextjs-pages) +- [Nuxt](/tutorials/username-and-password/nuxt) +- [SvelteKit](/tutorials/username-and-password/sveltekit) diff --git a/docs/pages/translations/ja/tutorials/username-and-password/nextjs-app.md b/docs/pages/translations/ja/tutorials/username-and-password/nextjs-app.md new file mode 100644 index 000000000..bb75dcdfb --- /dev/null +++ b/docs/pages/translations/ja/tutorials/username-and-password/nextjs-app.md @@ -0,0 +1,331 @@ +--- +title: "Username and password auth in Next.js App Router" +--- + +# Username and password auth in Next.js App Router + +Before starting, make sure you've set up your database as described in the [Getting started](/getting-started/nextjs-app) page. + +An [example project](https://github.com/lucia-auth/examples/tree/main/nextjs-app/username-and-password) based on this tutorial is also available. You can clone the example locally or [open it in StackBlitz](https://stackblitz.com/github/lucia-auth/examples/tree/v3/nextjs-app/username-and-password). + +``` +npx degit https://github.com/lucia-auth/examples/tree/main/nextjs-app/username-and-password +``` + +## Update database + +Add a `username` and `hashed_password` column to your user table. + +| column | type | attributes | +| ----------------- | -------- | ---------- | +| `username` | `string` | unique | +| `hashed_password` | `string` | | + +Create a `DatabaseUserAttributes` interface in the module declaration and add your database columns. By default, Lucia will not expose any database columns to the `User` type. To add a `username` field to it, use the `getUserAttributes()` option. + +```ts +import { Lucia } from "lucia"; + +export const lucia = new Lucia(adapter, { + sessionCookie: { + expires: false, + attributes: { + secure: process.env.NODE_ENV === "production" + } + }, + getUserAttributes: (attributes) => { + return { + // attributes has the type of DatabaseUserAttributes + username: attributes.username + }; + } +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: DatabaseUserAttributes; + } +} + +interface DatabaseUserAttributes { + username: string; +} +``` + +## Sign up user + +Create `app/signup/page.tsx` and set up a basic form and action. + +```tsx +export default async function Page() { + return ( + <> +

Create an account

+
+ + +
+ + +
+ +
+ + ); +} + +async function signup(_: any, formData: FormData): Promise {} + +interface ActionResult { + error: string; +} +``` + +In the form action, first do a very basic input validation. Hash the password, generate a new user ID, and create a new user. If successful, create a new session with `Lucia.createSession()` and set a new session cookie. + +```tsx +import { db } from "@/lib/db"; +import { Argon2id } from "oslo/password"; +import { cookies } from "next/headers"; +import { lucia } from "@/lib/auth"; +import { redirect } from "next/navigation"; +import { generateId } from "lucia"; + +export default async function Page() {} + +async function signup(_: any, formData: FormData): Promise { + "use server"; + const username = formData.get("username"); + // username must be between 4 ~ 31 characters, and only consists of lowercase letters, 0-9, -, and _ + // keep in mind some database (e.g. mysql) are case insensitive + if ( + typeof username !== "string" || + username.length < 3 || + username.length > 31 || + !/^[a-z0-9_-]+$/.test(username) + ) { + return { + error: "Invalid username" + }; + } + const password = formData.get("password"); + if (typeof password !== "string" || password.length < 6 || password.length > 255) { + return { + error: "Invalid password" + }; + } + + const hashedPassword = await new Argon2id().hash(password); + const userId = generateId(15); + + // TODO: check if username is already used + await db.table("user").insert({ + id: userId, + username: username, + hashed_password: hashedPassword + }); + + const session = await lucia.createSession(userId, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + return redirect("/"); +} +``` + +We recommend using Argon2id, but Oslo also provides Scrypt and Bcrypt. These only work in Node.js. If you're planning to deploy your project to a non-Node.js runtime, use `Scrypt` provided by `lucia`. This is a pure JS implementation but 2~3 times slower. For Bun, use [`Bun.password`](https://bun.sh/docs/api/hashing#bun-password). + +```ts +import { Scrypt } from "lucia"; + +new Scrypt().hash(password); +``` + +**If you're using Bcrypt, [set the maximum password length to 64 _bytes_](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#input-limits-of-bcrypt).** + +```ts +const length = new TextEncoder().encode(password).length; +``` + +## Sign in user + +Create `app/login/page.tsx` and set up a basic form and action. + +```tsx +// app/login/page.tsx +export default async function Page() { + return ( + <> +

Sign in

+
+ + +
+ + +
+ +
+ + ); +} + +async function login(_: any, formData: FormData): Promise {} + +interface ActionResult { + error: string; +} +``` + +In the form action, first do a very basic input validation. Get the user with the username and verify the password. If successful, create a new session with `Lucia.createSession()` and set a new session cookie. + +```tsx +import { Argon2id } from "oslo/password"; +import { cookies } from "next/headers"; +import { lucia } from "@/lib/auth"; +import { redirect } from "next/navigation"; + +export default async function Page() {} + +async function login(_: any, formData: FormData): Promise { + "use server"; + const username = formData.get("username"); + if ( + typeof username !== "string" || + username.length < 3 || + username.length > 31 || + !/^[a-z0-9_-]+$/.test(username) + ) { + return { + error: "Invalid username" + }; + } + const password = formData.get("password"); + if (typeof password !== "string" || password.length < 6 || password.length > 255) { + return { + error: "Invalid password" + }; + } + + const existingUser = await db + .table("username") + .where("username", "=", username.toLowerCase()) + .get(); + if (!existingUser) { + // NOTE: + // Returning immediately allows malicious actors to figure out valid usernames from response times, + // allowing them to only focus on guessing passwords in brute-force attacks. + // As a preventive measure, you may want to hash passwords even for invalid usernames. + // However, valid usernames can be already be revealed with the signup page among other methods. + // It will also be much more resource intensive. + // Since protecting against this is none-trivial, + // it is crucial your implementation is protected against brute-force attacks with login throttling etc. + // If usernames are public, you may outright tell the user that the username is invalid. + return { + error: "Incorrect username or password" + }; + } + + const validPassword = await new Argon2id().verify(existingUser.password, password); + if (!validPassword) { + return { + error: "Incorrect username or password" + }; + } + + const session = await lucia.createSession(existingUser.id, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + return redirect("/"); +} +``` + +## Validate requests + +Create `validateRequest()`. This will check for the session cookie, validate it, and set a new cookie if necessary. Make sure to catch errors when setting cookies and wrap the function with `cache()` to prevent unnecessary database calls. To learn more, see the [Validating requests](/basics/validate-session-cookies/nextjs-app) page. + +CSRF protection should be implemented but Next.js handles it when using form actions (but not for API routes). + +```ts +import { cookies } from "next/headers"; +import { cache } from "react"; + +import type { Session, User } from "lucia"; + +export const lucia = new Lucia(); + +export const validateRequest = cache( + async (): Promise<{ user: User; session: Session } | { user: null; session: null }> => { + const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null; + if (!sessionId) { + return { + user: null, + session: null + }; + } + + const result = await lucia.validateSession(sessionId); + // next.js throws when you attempt to set cookie when rendering page + try { + if (result.session && result.session.fresh) { + const sessionCookie = lucia.createSessionCookie(result.session.id); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + } + if (!result.session) { + const sessionCookie = lucia.createBlankSessionCookie(); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + } + } catch {} + return result; + } +); +``` + +This function can then be used in server components and form actions to get the current session and user. + +```tsx +import { redirect } from "next/navigation"; +import { validateRequest } from "@/lib/auth"; + +export default async function Page() { + const { user } = await validateRequest(); + if (!user) { + return redirect("/login"); + } + return

Hi, {user.username}!

; +} +``` + +## Sign out + +Sign out users by invalidating their session with `Lucia.invalidateSession()`. Make sure to remove their session cookie by setting a blank session cookie created with `Lucia.createBlankSessionCookie()`. + +```tsx +import { lucia, validateRequest } from "@/lib/auth"; +import { redirect } from "next/navigation"; +import { cookies } from "next/headers"; + +export default async function Page() { + return ( +
+ +
+ ); +} + +async function logout(): Promise { + "use server"; + const { session } = await validateRequest(); + if (!session) { + return { + error: "Unauthorized" + }; + } + + await lucia.invalidateSession(session.id); + + const sessionCookie = lucia.createBlankSessionCookie(); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + return redirect("/login"); +} +``` diff --git a/docs/pages/translations/ja/tutorials/username-and-password/nextjs-pages.md b/docs/pages/translations/ja/tutorials/username-and-password/nextjs-pages.md new file mode 100644 index 000000000..c3831adca --- /dev/null +++ b/docs/pages/translations/ja/tutorials/username-and-password/nextjs-pages.md @@ -0,0 +1,394 @@ +--- +title: "Tutorial: Username and password auth in Next.js Pages router" +--- + +# Tutorial: Username and password auth in Next.js Pages router + +Before starting, make sure you've set up your database and middleware as described in the [Getting started](/getting-started/nextjs-pages) page. + +An [example project](https://github.com/lucia-auth/examples/tree/main/nextjs-pages/username-and-password) based on this tutorial is also available. You can clone the example locally or [open it in StackBlitz](https://stackblitz.com/github/lucia-auth/examples/tree/v3/nextjs-pages/username-and-password). + +``` +npx degit https://github.com/lucia-auth/examples/tree/main/nextjs-pages/username-and-password +``` + +## Update database + +Add a `username` and `hashed_password` column to your user table. + +| column | type | attributes | +| ----------------- | -------- | ---------- | +| `username` | `string` | unique | +| `hashed_password` | `string` | | + +Create a `DatabaseUserAttributes` interface in the module declaration and add your database columns. By default, Lucia will not expose any database columns to the `User` type. To add a `username` field to it, use the `getUserAttributes()` option. + +```ts +import { Lucia } from "lucia"; + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: process.env.NODE_ENV === "production" + } + }, + getUserAttributes: (attributes) => { + return { + // attributes has the type of DatabaseUserAttributes + username: attributes.username + }; + } +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: DatabaseUserAttributes; + } +} + +interface DatabaseUserAttributes { + username: string; +} +``` + +## Sign up user + +Create `pages/signup.tsx` and set up a basic form. + +```tsx +// pages/signup.tsx +import { useRouter } from "next/router"; +import type { FormEvent } from "react"; + +export default function Page() { + const router = useRouter(); + + async function onSubmit(e: FormEvent) { + e.preventDefault(); + const formElement = e.target as HTMLFormElement; + const response = await fetch(formElement.action, { + method: formElement.method, + body: JSON.stringify(Object.fromEntries(new FormData(formElement).entries())), + headers: { + "Content-Type": "application/json" + } + }); + if (response.ok) { + router.push("/"); + } + } + + return ( + <> +

Create an account

+
+ + +
+ + +
+ +
+ + ); +} +``` + +Create an API route in `pages/api/signup.ts`. First, do a very basic input validation. Hash the password, generate a new user ID, and create a new user. If successful, create a new session with `Lucia.createSession()` and set a new session cookie. + +```ts +// pages/api/signup.ts +import { lucia } from "@/lib/auth"; +import { generateId } from "lucia"; +import { Argon2id } from "oslo/password"; + +import type { NextApiRequest, NextApiResponse } from "next"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "POST") { + res.status(404).end(); + return; + } + + const body: null | Partial<{ username: string; password: string }> = req.body; + const username = body?.username; + if (!username || username.length < 3 || username.length > 31 || !/^[a-z0-9_-]+$/.test(username)) { + res.status(400).json({ + error: "Invalid username" + }); + return; + } + const password = body?.password; + if (!password || password.length < 6 || password.length > 255) { + res.status(400).json({ + error: "Invalid password" + }); + return; + } + + const hashedPassword = await new Argon2id().hash(password); + const userId = generateId(15); + + // TODO: check if username is already used + await db.table("user").insert({ + id: userId, + username: username, + hashed_password: hashedPassword + }); + + const session = await lucia.createSession(userId, {}); + res + .appendHeader("Set-Cookie", lucia.createSessionCookie(session.id).serialize()) + .status(200) + .end(); +} +``` + +We recommend using Argon2id, but Oslo also provides Scrypt and Bcrypt. These only work in Node.js. If you're planning to deploy your project to a non-Node.js runtime, use `Scrypt` provided by `lucia`. This is a pure JS implementation but 2~3 times slower. For Bun, use [`Bun.password`](https://bun.sh/docs/api/hashing#bun-password). + +```ts +import { Scrypt } from "lucia"; + +new Scrypt().hash(password); +``` + +**If you're using Bcrypt, [set the maximum password length to 64 _bytes_](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#input-limits-of-bcrypt).** + +```ts +const length = new TextEncoder().encode(password).length; +``` + +## Sign in user + +Create `pages/login.tsx` and set up a basic form. + +```tsx +// pages/signup.tsx +import { useRouter } from "next/router"; +import type { FormEvent } from "react"; + +export default function Page() { + const router = useRouter(); + + async function onSubmit(e: FormEvent) { + e.preventDefault(); + const formElement = e.target as HTMLFormElement; + const response = await fetch(formElement.action, { + method: formElement.method, + body: JSON.stringify(Object.fromEntries(new FormData(formElement).entries())), + headers: { + "Content-Type": "application/json" + } + }); + if (response.ok) { + router.push("/"); + } + } + + return ( + <> +

Create an account

+
+ + +
+ + +
+ +
+ + ); +} +``` + +Create an API route as `pages/api/signup.ts`. First, do a very basic input validation. Get the user with the username and verify the password. If successful, create a new session with `Lucia.createSession()` and set a new session cookie. + +```ts +// pages/api/login.ts +import { Argon2id } from "oslo/password"; +import { lucia } from "@/lib/auth"; + +import type { NextApiRequest, NextApiResponse } from "next"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "POST") { + res.status(404).end(); + return; + } + + const body: null | Partial<{ username: string; password: string }> = req.body; + const username = body?.username; + if (!username || username.length < 3 || username.length > 31 || !/^[a-z0-9_-]+$/.test(username)) { + res.status(400).json({ + error: "Invalid username" + }); + return; + } + const password = body?.password; + if (!password || password.length < 6 || password.length > 255) { + res.status(400).json({ + error: "Invalid password" + }); + return; + } + + const existingUser = await db + .table("username") + .where("username", "=", username.toLowerCase()) + .get(); + if (!existingUser) { + // NOTE: + // Returning immediately allows malicious actors to figure out valid usernames from response times, + // allowing them to only focus on guessing passwords in brute-force attacks. + // As a preventive measure, you may want to hash passwords even for invalid usernames. + // However, valid usernames can be already be revealed with the signup page among other methods. + // It will also be much more resource intensive. + // Since protecting against this is none-trivial, + // it is crucial your implementation is protected against brute-force attacks with login throttling etc. + // If usernames are public, you may outright tell the user that the username is invalid. + res.status(400).json({ + error: "Incorrect username or password" + }); + return; + } + + const validPassword = await new Argon2id().verify(existingUser.password, password); + if (!validPassword) { + res.status(400).json({ + error: "Incorrect username or password" + }); + return; + } + + const session = await lucia.createSession(existingUser.id, {}); + res + .appendHeader("Set-Cookie", lucia.createSessionCookie(session.id).serialize()) + .status(200) + .end(); +} +``` + +## Validate requests + +Create `validateRequest()`. This will check for the session cookie, validate it, and set a new cookie if necessary. To learn more, see the [Validating requests](/basics/validate-session-cookies/nextjs-pages) page. + +CSRF protection should be implemented and you should already have a middleware for it. + +```ts +import type { Session, User } from "lucia"; +import type { IncomingMessage, ServerResponse } from "http"; + +export const lucia = new Lucia(); + +export async function validateRequest( + req: IncomingMessage, + res: ServerResponse +): Promise<{ user: User; session: Session } | { user: null; session: null }> { + const sessionId = lucia.readSessionCookie(req.headers.cookie ?? ""); + if (!sessionId) { + return { + user: null, + session: null + }; + } + const result = await lucia.validateSession(sessionId); + if (result.session && result.session.fresh) { + res.appendHeader("Set-Cookie", lucia.createSessionCookie(result.session.id).serialize()); + } + if (!result.session) { + res.appendHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize()); + } + return result; +} +``` + +This function can then be used in both `getServerSideProps()` and API routes. + +```tsx +import { validateRequest } from "@/lib/auth"; + +import type { + GetServerSidePropsContext, + GetServerSidePropsResult, + InferGetServerSidePropsType +} from "next"; +import type { User } from "lucia"; + +export async function getServerSideProps(context: GetServerSidePropsContext): Promise< + GetServerSidePropsResult<{ + user: User; + }> +> { + const { user } = await validateRequest(context.req, context.res); + if (!user) { + return { + redirect: { + permanent: false, + destination: "/login" + } + }; + } + return { + props: { + user + } + }; +} + +export default function Page({ user }: InferGetServerSidePropsType) { + return

Hi, {user.username}!

; +} +``` + +## Sign out + +Sign out users by invalidating their session with `Lucia.invalidateSession()`. Make sure to remove their session cookie by setting a blank session cookie created with `Lucia.createBlankSessionCookie()`. + +```ts +// pages/api/logout.ts +import { lucia, validateRequest } from "@/lib/auth"; + +import type { NextApiRequest, NextApiResponse } from "next"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "POST") { + res.status(404).end(); + return; + } + const { session } = await validateRequest(req, res); + if (!session) { + res.status(401).end(); + return; + } + await lucia.invalidateSession(session.id); + res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize()).status(200).end(); +} +``` + +```tsx +import { useRouter } from "next/router"; + +import type { FormEvent } from "react"; + +export default function Page({ user }: InferGetServerSidePropsType) { + const router = useRouter(); + + async function onSubmit(e: FormEvent) { + e.preventDefault(); + const formElement = e.target as HTMLFormElement; + await fetch(formElement.action, { + method: formElement.method + }); + router.push("/login"); + } + + return ( +
+ +
+ ); +} +``` diff --git a/docs/pages/translations/ja/tutorials/username-and-password/nuxt.md b/docs/pages/translations/ja/tutorials/username-and-password/nuxt.md new file mode 100644 index 000000000..4d2adcf52 --- /dev/null +++ b/docs/pages/translations/ja/tutorials/username-and-password/nuxt.md @@ -0,0 +1,326 @@ +--- +title: "Tutorial: Username and password auth in Nuxt" +--- + +# Tutorial: Username and password auth in Nuxt + +Before starting, make sure you've set up your database and middleware as described in the [Getting started](/getting-started/nuxt) page. + +An [example project](https://github.com/lucia-auth/examples/tree/main/nuxt/username-and-password) based on this tutorial is also available. You can clone the example locally or [open it in StackBlitz](https://stackblitz.com/github/lucia-auth/examples/tree/v3/nuxt/username-and-password). + +``` +npx degit https://github.com/lucia-auth/examples/tree/main/nuxt/username-and-password +``` + +## Update database + +Add a `username` and `hashed_password` column to your user table. + +| column | type | attributes | +| ----------------- | -------- | ---------- | +| `username` | `string` | unique | +| `hashed_password` | `string` | | + +Create a `DatabaseUserAttributes` interface in the module declaration and add your database columns. By default, Lucia will not expose any database columns to the `User` type. To add a `username` field to it, use the `getUserAttributes()` option. + +```ts +// server/utils/auth.ts +import { Lucia } from "lucia"; + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: !import.meta.dev + } + }, + getUserAttributes: (attributes) => { + return { + // attributes has the type of DatabaseUserAttributes + username: attributes.username + }; + } +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: DatabaseUserAttributes; + } +} + +interface DatabaseUserAttributes { + username: string; +} +``` + +## Sign up user + +Create `pages/signup.nuxt` and set up a basic form. + +```vue + + + + +``` + +Create an API route in `server/api/signup.post.ts`. First, do a very basic input validation. Hash the password, generate a new user ID, and create a new user. If successful, create a new session with `Lucia.createSession()` and set a new session cookie. + +```ts +// server/api/signup.post.ts +import { Argon2id } from "oslo/password"; +import { generateId } from "lucia"; +import { SqliteError } from "better-sqlite3"; + +export default eventHandler(async (event) => { + const formData = await readFormData(event); + const username = formData.get("username"); + if ( + typeof username !== "string" || + username.length < 3 || + username.length > 31 || + !/^[a-z0-9_-]+$/.test(username) + ) { + throw createError({ + message: "Invalid username", + statusCode: 400 + }); + } + const password = formData.get("password"); + if (typeof password !== "string" || password.length < 6 || password.length > 255) { + throw createError({ + message: "Invalid password", + statusCode: 400 + }); + } + + const hashedPassword = await new Argon2id().hash(password); + const userId = generateId(15); + + // TODO: check if username is already used + await db.table("user").insert({ + id: userId, + username: username, + hashed_password: hashedPassword + }); + + const session = await lucia.createSession(userId, {}); + appendHeader(event, "Set-Cookie", lucia.createSessionCookie(session.id).serialize()); +}); +``` + +We recommend using Argon2id, but Oslo also provides Scrypt and Bcrypt. These only work in Node.js. If you're planning to deploy your project to a non-Node.js runtime, use `Scrypt` provided by `lucia`. This is a pure JS implementation but 2~3 times slower. For Bun, use [`Bun.password`](https://bun.sh/docs/api/hashing#bun-password). + +```ts +import { Scrypt } from "lucia"; + +new Scrypt().hash(password); +``` + +**If you're using Bcrypt, [set the maximum password length to 64 _bytes_](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#input-limits-of-bcrypt).** + +```ts +const length = new TextEncoder().encode(password).length; +``` + +## Sign in user + +Create `pages/login.vue` and set up a basic form. + +```vue + + + + +``` + +Create an API route as `server/api/login.post.ts`. First, do a very basic input validation. Get the user with the username and verify the password. If successful, create a new session with `Lucia.createSession()` and set a new session cookie. + +```ts +// server/api/login.post.ts +import { Argon2id } from "oslo/password"; + +export default eventHandler(async (event) => { + const formData = await readFormData(event); + const username = formData.get("username"); + if ( + typeof username !== "string" || + username.length < 3 || + username.length > 31 || + !/^[a-z0-9_-]+$/.test(username) + ) { + throw createError({ + message: "Invalid username", + statusCode: 400 + }); + } + const password = formData.get("password"); + if (typeof password !== "string" || password.length < 6 || password.length > 255) { + throw createError({ + message: "Invalid password", + statusCode: 400 + }); + } + + const existingUser = await db + .table("username") + .where("username", "=", username.toLowerCase()) + .get(); + if (!existingUser) { + // NOTE: + // Returning immediately allows malicious actors to figure out valid usernames from response times, + // allowing them to only focus on guessing passwords in brute-force attacks. + // As a preventive measure, you may want to hash passwords even for invalid usernames. + // However, valid usernames can be already be revealed with the signup page among other methods. + // It will also be much more resource intensive. + // Since protecting against this is none-trivial, + // it is crucial your implementation is protected against brute-force attacks with login throttling etc. + // If usernames are public, you may outright tell the user that the username is invalid. + throw createError({ + message: "Incorrect username or password", + statusCode: 400 + }); + } + + const validPassword = await new Argon2id().verify(existingUser.password, password); + if (!validPassword) { + throw createError({ + message: "Incorrect username or password", + statusCode: 400 + }); + } + + const session = await lucia.createSession(existingUser.id, {}); + appendHeader(event, "Set-Cookie", lucia.createSessionCookie(session.id).serialize()); +}); +``` + +## Validate requests + +You can validate requests by checking `event.context.user`. The field `user.username` is available since we defined the `getUserAttributes()` option. You can protect pages, such as `/`, by redirecting unauthenticated users to the login page. + +```ts +export default defineEventHandler((event) => { + if (event.context.user) { + const username = event.context.user.username; + } + // ... +}); +``` + +## Get user in the client + +Create an API route in `server/api/user.get.ts`. This will just return the current user. + +```ts +// server/api/user.get.ts +export default defineEventHandler((event) => { + return event.context.user; +}); +``` + +Create a composable `useUser()` in `composables/auth.ts`. + +```ts +// composables/auth.ts +import type { User } from "lucia"; + +export const useUser = () => { + const user = useState("user", () => null); + return user; +}; +``` + +Then, create a global middleware in `middleware/auth.global.ts` to populate it. + +```ts +// middleware/auth.global.ts +export default defineNuxtRouteMiddleware(async () => { + const user = useUser(); + user.value = await $fetch("/api/user"); +}); +``` + +You can now use `useUser()` client side to get the current user. + +```vue + +``` + +## Sign out + +Sign out users by invalidating their session with `Lucia.invalidateSession()`. Make sure to remove their session cookie by setting a blank session cookie created with `Lucia.createBlankSessionCookie()`. + +```ts +// server/api/logout.post.ts +export default eventHandler(async (event) => { + if (!event.context.session) { + throw createError({ + statusCode: 403 + }); + } + await lucia.invalidateSession(event.context.session.id); + appendHeader(event, "Set-Cookie", lucia.createBlankSessionCookie().serialize()); +}); +``` + +```vue + + + +``` diff --git a/docs/pages/translations/ja/tutorials/username-and-password/sveltekit.md b/docs/pages/translations/ja/tutorials/username-and-password/sveltekit.md new file mode 100644 index 000000000..f63e9ee2e --- /dev/null +++ b/docs/pages/translations/ja/tutorials/username-and-password/sveltekit.md @@ -0,0 +1,292 @@ +--- +title: "Tutorial: Username and password auth in SvelteKit" +--- + +# Tutorial: Username and password auth in SvelteKit + +Before starting, make sure you've set up your database and middleware as described in the [Getting started](/getting-started/astro) page. + +An [example project](https://github.com/lucia-auth/examples/tree/main/sveltekit/username-and-password) based on this tutorial is also available. You can clone the example locally or [open it in StackBlitz](https://stackblitz.com/github/lucia-auth/examples/tree/main/sveltekit/username-and-password). + +``` +npx degit https://github.com/lucia-auth/examples/tree/main/sveltekit/username-and-password +``` + +## Update database + +Add a `username` and `hashed_password` column to your user table. + +| column | type | attributes | +| ----------------- | -------- | ---------- | +| `username` | `string` | unique | +| `hashed_password` | `string` | | + +Create a `DatabaseUserAttributes` interface in the module declaration and add your database columns. By default, Lucia will not expose any database columns to the `User` type. To add a `username` field to it, use the `getUserAttributes()` option. + +```ts +// src/lib/server/auth.ts +import { Lucia } from "lucia"; +import { dev } from "$app/environment"; + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: !dev + } + }, + getUserAttributes: (attributes) => { + return { + // attributes has the type of DatabaseUserAttributes + username: attributes.username + }; + } +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: DatabaseUserAttributes; + } +} + +interface DatabaseUserAttributes { + username: string; +} +``` + +## Sign up user + +Create `routes/signup/+page.svelte` and set up a basic form. + +```svelte + + + +

Sign up

+
+ +
+ +
+ +
+``` + +Create a form action in `routes/signup/+page.server.ts`. First, do a very basic input validation. Hash the password, generate a new user ID, and create a new user. If successful, create a new session with `Lucia.createSession()` and set a new session cookie. + +```ts +// routes/signup/+page.server.ts +import { lucia } from "$lib/server/auth"; +import { fail, redirect } from "@sveltejs/kit"; +import { generateId } from "lucia"; +import { Argon2id } from "oslo/password"; + +import type { Actions } from "./$types"; + +export const actions: Actions = { + default: async (event) => { + const formData = await event.request.formData(); + const username = formData.get("username"); + const password = formData.get("password"); + // username must be between 4 ~ 31 characters, and only consists of lowercase letters, 0-9, -, and _ + // keep in mind some database (e.g. mysql) are case insensitive + if ( + typeof username !== "string" || + username.length < 3 || + username.length > 31 || + !/^[a-z0-9_-]+$/.test(username) + ) { + return fail(400, { + message: "Invalid username" + }); + } + if (typeof password !== "string" || password.length < 6 || password.length > 255) { + return fail(400, { + message: "Invalid password" + }); + } + + const userId = generateId(15); + const hashedPassword = await new Argon2id().hash(password); + + // TODO: check if username is already used + await db.table("user").insert({ + id: userId, + username: username, + hashed_password: hashedPassword + }); + + const session = await lucia.createSession(userId, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + event.cookies.set(sessionCookie.name, sessionCookie.value, { + path: ".", + ...sessionCookie.attributes + }); + + redirect(302, "/"); + } +}; +``` + +We recommend using Argon2id, but Oslo also provides Scrypt and Bcrypt. These only work in Node.js. If you're planning to deploy your project to a non-Node.js runtime, use `Scrypt` provided by `lucia`. This is a pure JS implementation but 2~3 times slower. For Bun, use [`Bun.password`](https://bun.sh/docs/api/hashing#bun-password). + +```ts +import { Scrypt } from "lucia"; + +new Scrypt().hash(password); +``` + +**If you're using Bcrypt, [set the maximum password length to 64 _bytes_](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#input-limits-of-bcrypt).** + +```ts +const length = new TextEncoder().encode(password).length; +``` + +## Sign in user + +Create `routes/login/+page.svelte` and set up a basic form. + +```svelte + + + +

Sign in

+
+ +
+ +
+ +
+``` + +Create an API route as `pages/api/signup.ts`. First, do a very basic input validation. Get the user with the username and verify the password. If successful, create a new session with `Lucia.createSession()` and set a new session cookie. + +```ts +import { lucia } from "$lib/server/auth"; +import { fail, redirect } from "@sveltejs/kit"; +import { Argon2id } from "oslo/password"; + +import type { Actions } from "./$types"; + +export const actions: Actions = { + default: async (event) => { + const formData = await event.request.formData(); + const username = formData.get("username"); + const password = formData.get("password"); + + if ( + typeof username !== "string" || + username.length < 3 || + username.length > 31 || + !/^[a-z0-9_-]+$/.test(username) + ) { + return fail(400, { + message: "Invalid username" + }); + } + if (typeof password !== "string" || password.length < 6 || password.length > 255) { + return fail(400, { + message: "Invalid password" + }); + } + + const existingUser = await db + .table("username") + .where("username", "=", username.toLowerCase()) + .get(); + if (!existingUser) { + // NOTE: + // Returning immediately allows malicious actors to figure out valid usernames from response times, + // allowing them to only focus on guessing passwords in brute-force attacks. + // As a preventive measure, you may want to hash passwords even for invalid usernames. + // However, valid usernames can be already be revealed with the signup page among other methods. + // It will also be much more resource intensive. + // Since protecting against this is none-trivial, + // it is crucial your implementation is protected against brute-force attacks with login throttling etc. + // If usernames are public, you may outright tell the user that the username is invalid. + return fail(400, { + message: "Incorrect username or password" + }); + } + + const validPassword = await new Argon2id().verify(existingUser.hashed_password, password); + if (!validPassword) { + return fail(400, { + message: "Incorrect username or password" + }); + } + + const session = await lucia.createSession(existingUser.id, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + event.cookies.set(sessionCookie.name, sessionCookie.value, { + path: ".", + ...sessionCookie.attributes + }); + + redirect(302, "/"); + } +}; +``` + +## Validate requests + +You can validate requests by checking `locals.user`. The field `user.username` is available since we defined the `getUserAttributes()` option. You can protect pages, such as `/`, by redirecting unauthenticated users to the login page. + +```ts +// +page.server.ts +import type { PageServerLoad, Actions } from "./$types"; + +export const load: PageServerLoad = async (event) => { + if (!event.locals.user) redirect(302, "/login"); + return { + username: event.locals.user.username + }; +}; +``` + +## Sign out user + +Sign out users by invalidating their session with `Lucia.invalidateSession()`. Make sure to remove their session cookie by setting a blank session cookie created with `Lucia.createBlankSessionCookie()`. + +```ts +// routes/+page.server.ts +import { lucia } from "$lib/server/auth"; +import { fail, redirect } from "@sveltejs/kit"; + +import type { Actions, PageServerLoad } from "./$types"; + +export const load: PageServerLoad = async ({ locals }) => { + // ... +}; + +export const actions: Actions = { + default: async (event) => { + if (!event.locals.session) { + return fail(401); + } + await lucia.invalidateSession(event.locals.session.id); + const sessionCookie = lucia.createBlankSessionCookie(); + event.cookies.set(sessionCookie.name, sessionCookie.value, { + path: ".", + ...sessionCookie.attributes + }); + redirect(302, "/login"); + } +}; +``` + +```svelte + + + +
+ +
+``` diff --git a/docs/pages/translations/ja/upgrade-v3/index.md b/docs/pages/translations/ja/upgrade-v3/index.md new file mode 100644 index 000000000..716e0ffee --- /dev/null +++ b/docs/pages/translations/ja/upgrade-v3/index.md @@ -0,0 +1,213 @@ +--- +title: "Upgrade to Lucia v3" +--- + +# Upgrade to Lucia v3 + +Version 3.0 rethinks Lucia and the role it should play in your application. We have stripped out all the annoying bits, and everything else we kept has been refined even more. Everything is more flexible, and just all around easier to understand and work with. + +We estimate it will take about an hour or two to upgrade your project, though it depends on how big your application is. If you're having issues with the migration or have any questions, feel free to ask on our [Discord server](https://discord.com/invite/PwrK3kpVR3). + +## Major changes + +The biggest change to Lucia is that keys have been removed entirely. We believe it was too limiting and ultimately an unnecessary concept that made many projects more complex than they needed to be. Another big change is that Lucia no longer handles user creation, so `createUser()` among other APIs has been removed. + +For a simple password-based auth, the password can just be stored in the user table. + +```ts +const hashedPassword = await new Argon2id().hash(password); +const userId = generateId(15); + +await db.table("user").insert({ + id: userId, + email, + hashed_password: hashedPassword +}); +``` + +Another change is that APIs for request handling have been removed. We now just provide code snippets in the docs that you can copy-paste. + +Lucia is now built with [Oslo](https://oslo.js.org), a library that provides useful auth-related utilities. While not required, we recommend installing it alongside Lucia as all guides in the documentation use it some way or another. + +``` +npm install lucia oslo +``` + +## Initialize Lucia + +Here's the base config. Lucia is now initialized using the `Lucia` class, which takes an adapter and an options object. **Make sure to configure the `sessionCookie` option**. + +```ts +import { Lucia, TimeSpan } from "lucia"; +import { astro } from "lucia/middleware"; + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: env === "PRODUCTION" // replaces `env` config + } + } +}); +``` + +Here's the fully updated configuration for reference. `middleware` and `csrfProtection` have been removed. + +```ts +import { Lucia, TimeSpan } from "lucia"; +import { astro } from "lucia/middleware"; + +export const lucia = new Lucia(adapter, { + getSessionAttributes: (attributes) => { + return { + ipCountry: attributes.ip_country + }; + }, + getUserAttributes: (attributes) => { + return { + username: attributes.username + }; + }, + sessionExpiresIn: new TimeSpan(30, "d"), // no more active/idle + sessionCookie: { + name: "session", + expires: false, // session cookies have very long lifespan (2 years) + attributes: { + secure: true, + sameSite: "strict", + domain: "example.com" + } + } +}); +``` + +### Type declaration + +Lucia v3 uses the newer module syntax instead of `.d.ts` files for declaring types for improved ergonomics and monorepo support. The `Lucia` type declaration is required. + +```ts +export const lucia = new Lucia(); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseSessionAttributes: DatabaseSessionAttributes; + DatabaseUserAttributes: DatabaseUserAttributes; + } +} + +interface DatabaseSessionAttributes { + country: string; +} +interface DatabaseUserAttributes { + username: string; +} +``` + +### Polyfill + +`lucia/polyfill/node` has been removed. Manually polyfill the Web Crypto API by importing the `crypto` module. + +```ts +import { webcrypto } from "node:crypto"; + +globalThis.crypto = webcrypto as Crypto; +``` + +## Update your database + +Refer to each database migration guide: + +- [Mongoose](/upgrade-v3/mongoose) +- [MySQL](/upgrade-v3/mysql) +- [PostgreSQL](/upgrade-v3/postgresql) +- [Prisma](/upgrade-v3/prisma) +- [SQLite](/upgrade-v3/sqlite) + +The following packages are deprecated: + +- `@lucia-auth/adapter-mongoose` (see Mongoose migration guide) +- `@lucia-auth/adapter-session-redis` +- `@lucia-auth/adapter-session-unstorage` + +If you're using a session adapter, we recommend building a custom adapter as the API has been greatly simplified. + +## Sessions + +### Session validation + +Middleware, `Auth.handleRequest()`, and `AuthRequest` have been removed. **This means Lucia no longer provides strict CSRF protection**. For replacing `AuthRequest.validate()`, see the [Validating session cookies](/guides/validate-session-cookies) guide or a framework-specific version of it as these need to be re-implemented from scratch (though it's just copy-pasting code from the guides): + +- [Astro](/guides/validate-session-cookies/astro) +- [Elysia](/guides/validate-session-cookies/elysia) +- [Express](/guides/validate-session-cookies/express) +- [Hono](/guides/validate-session-cookies/hono) +- [Next.js App router](/guides/validate-session-cookies/nextjs-app) +- [Next.js Pages router](/guides/validate-session-cookies/nextjs-pages) +- [Nuxt](/guides/validate-session-cookies/nuxt) +- [SvelteKit](/guides/validate-session-cookies/sveltekit) + +`Session.sessionId` has been renamed to `Session.id` + +```ts +const sessionId = session.id; +``` + +`validateSession()` no longer throws an error when the session is invalid, and returns an object of `User` and `Session` instead. + +```ts +// v3 +const { session, user } = await auth.validateSession(sessionId); +if (!session) { + // invalid session +} +``` + +### Session cookies + +`createSessionCookie()` now takes a session ID instead of a session object, and `createBlankSessionCookie()` should be used for creating blank session cookies. + +```ts +const sessionCookie = auth.createSessionCookie(session.id); +const blankSessionCookie = auth.createBlankSessionCookie(); +``` + +## Update authentication + +Refer to these guides: + +- [Upgrade OAuth setup to v3](/upgrade-v3/oauth) +- [Upgrade Password-based auth to v3](/upgrade-v3/password) + +## Framework specific configuration + +If you installed Oslo, you must prevent `oslo` from getting bundled. This is only required when using the `oslo/password` module. + +### Astro + +```ts +// astro.config.mjs +export default defineConfig({ + // ... + vite: { + optimizeDeps: { + exclude: ["oslo"] + } + } +}); +``` + +### Next.js + +**`oslo/password` does NOT work with Turbopack.** + +```ts +// next.config.ts +const nextConfig = { + webpack: (config) => { + config.externals.push("@node-rs/argon2", "@node-rs/bcrypt"); + return config; + } +}; +``` + + diff --git a/docs/pages/translations/ja/upgrade-v3/mongoose.md b/docs/pages/translations/ja/upgrade-v3/mongoose.md new file mode 100644 index 000000000..71d04d86a --- /dev/null +++ b/docs/pages/translations/ja/upgrade-v3/mongoose.md @@ -0,0 +1,173 @@ +--- +title: "Upgrade your Mongoose project to v3" +--- + +# Upgrade your Mongoose project to v3 + +Read this guide carefully as some parts depend on your current structure (**especially the collection names**), and feel free to ask questions on our Discord server if you have any questions. + +## Update the adapter + +The Mongoose adapter has been replaced with the MongoDB adapter. + +``` +npm install @lucia-auth/adapter-mongodb +``` + +Initialize the adapter: + +```ts +import { MongoDBAdapter } from "@lucia-auth/adapter-mongodb"; +import mongoose from "mongoose"; + +const adapter = new MongodbAdapter( + mongoose.connection.collection("sessions"), + mongoose.connection.collection("users") +); +``` + +## Update the session collection + +Replace the `idle_expires` field with `expires_at` and update the Mongoose schema accordingly. + +```ts +db.sessions.updateMany({}, [ + { + $set: { + expires_at: { $toDate: "$idle_expires" } + } + }, + { + $unset: ["idle_expires", "active_expires"] + } +]); +``` + +```ts +import mongoose from "mongoose"; + +const Session = mongoose.model( + "Session", + new mongoose.Schema( + { + _id: { + type: String, + required: true + }, + user_id: { + type: String, + required: true + }, + expires_at: { + type: Date, + required: true + } + } as const, + { _id: false } + ) +); +``` + +## Replace the key collection + +Keys have been removed. You can keep using them but you may want to update your schema to better align with MongoDB. + +### OAuth accounts + +This database command adds a `github_id` field to users with a GitHub account based on the key collection. + +```ts +db.users.aggregate([ + { + $lookup: { + from: "keys", + localField: "_id", + foreignField: "user_id", + as: "github_accounts", + pipeline: [ + { + $match: { + _id: { + $regex: /^github:/ + } + } + } + ] + } + }, + { + $match: { + $expr: { + $gt: [{ $size: "$github_accounts" }, 0] + } + } + }, + { + $set: { + github_id: { + $replaceOne: { + input: { $arrayElemAt: ["$github_accounts._id", 0] }, + find: "github:", + replacement: "" + } + } + } + }, + { + $unset: ["github_accounts"] + }, + { + $merge: { + into: "users", + whenMatched: "merge" + } + } +]); +``` + +### Password accounts + +This database command moves the `hashed_password` field from the keys collection to the users collection. + +```ts +db.users.aggregate([ + { + $lookup: { + from: "keys", + localField: "_id", + foreignField: "user_id", + as: "password_accounts", + pipeline: [ + { + $match: { + hashed_password: { + $ne: null + } + } + } + ] + } + }, + { + $match: { + $expr: { + $gt: [{ $size: "$password_accounts" }, 0] + } + } + }, + { + $set: { + hashed_password: { $arrayElemAt: ["$password_accounts.hashed_password", 0] } + } + }, + { + $unset: ["password_accounts"] + }, + { + $merge: { + into: "users", + whenMatched: "merge" + } + } +]); +``` diff --git a/docs/pages/translations/ja/upgrade-v3/mysql.md b/docs/pages/translations/ja/upgrade-v3/mysql.md new file mode 100644 index 000000000..3ee84bd7e --- /dev/null +++ b/docs/pages/translations/ja/upgrade-v3/mysql.md @@ -0,0 +1,98 @@ +--- +title: "Upgrade your MySQL database to v3" +--- + +# Upgrade your MySQL database to v3 + +**Migration must be handled manually or else you will lose all your data**. **Do NOT use automated tools as is.** Read this guide carefully as some parts depend on your current structure (**especially the table names**), and feel free to ask questions on our Discord server if you have any questions. + +## Update the adapter + +Install the latest version of the MySQL adapter package. + +``` +npm install @lucia-auth/adapter-mysql +``` + +Initialize the adapter: + +```ts +import { Mysql2Adapter, PlanetScaleAdapter } from "@lucia-auth/adapter-mysql"; + +new Mysql2Adapter(pool, { + // table names + user: "user", + session: "user_session" +}); + +new PlanetScaleAdapter(connection, { + // table names + user: "user", + session: "user_session" +}); +``` + +## Update session table + +The main change to the session table is that the `idle_expires` and `active_expires` columns are replaced with a single `expires_at` column. Unlike the previous columns, it's a `DATETIME` column. + +**Check your table names before running the code.** + +```sql +ALTER TABLE user_session ADD expires_at DATETIME; + +UPDATE user_session SET expires_at = FROM_UNIXTIME(idle_expires / 1000); + +ALTER TABLE user_session DROP active_expires, DROP idle_expires, MODIFY expires_at DATETIME NOT NULL; +``` + +You may also just delete the session table and replace it with the [new schema](/database/mysql#schema). + +## Replace key table + +You can keep using the key table, but we recommend using dedicated tables for each authentication method. + +### OAuth + +The SQL below creates a dedicated table `oauth_account` for storing all user OAuth accounts. This assumes all keys where `hashed_password` column is null are for OAuth accounts. You may also separate them by the OAuth provider. You should adjust the `VARCHAR` length accordingly. + +```sql +CREATE TABLE oauth_account ( + provider_id VARCHAR(255) NOT NULL, + provider_user_id VARCHAR(255) NOT NULL, + user_id VARCHAR(255) NOT NULL REFERENCES user(id), + PRIMARY KEY (provider_id, provider_user_id) +); + +INSERT INTO oauth_account (provider_id, provider_user_id, user_id) +SELECT SUBSTRING(id, 1, POSITION(':' IN id)-1), SUBSTRING(id, POSITION(':' IN id)+1), user_id FROM user_key +WHERE hashed_password IS NULL; +``` + +### Email/password + +The SQL below creates a dedicated table `password` for storing user passwords. This assumes the provider ID for emails was `email` and that you're already storing the users' emails in the user table. + +```sql +CREATE TABLE password ( + id INT PRIMARY KEY AUTO_INCREMENT, + hashed_password VARCHAR(255) NOT NULL, + user_id VARCHAR(255) NOT NULL REFERENCES user(id) +); + +INSERT INTO password (hashed_password, user_id) +SELECT hashed_password, user_id FROM user_key +WHERE SUBSTRING(id, 1, POSITION(':' IN id)-1) = 'email'; +``` + +Alternatively, you can store the user's credentials in the user table if you only work with email/password. + +```sql +ALTER TABLE user ADD hashed_password VARCHAR(255); + +UPDATE user INNER JOIN user_key ON user_key.user_id = user.id +SET user.hashed_password = user_key.hashed_password +WHERE user_key.hashed_password IS NOT NULL; + +ALTER TABLE user MODIFY hashed_password VARCHAR(255) NOT NULL; +``` diff --git a/docs/pages/translations/ja/upgrade-v3/oauth.md b/docs/pages/translations/ja/upgrade-v3/oauth.md new file mode 100644 index 000000000..cd00face5 --- /dev/null +++ b/docs/pages/translations/ja/upgrade-v3/oauth.md @@ -0,0 +1,152 @@ +--- +title: "Upgrade OAuth setup to v3" +--- + +# Upgrade OAuth setup to v3 + +## Update database + +You can continue using the keys table but we recommend creating a dedicated table for storing OAuth accounts, as shown in the database migration guides. + +## Replace OAuth integration + +The OAuth integration has been replaced with [Arctic](https://github.com/pilcrowonpaper/arctic), which provides everything the integration did without Lucia-specific APIs. It supports all the OAuth providers that the integration supported. + +``` +npm install arctic +``` + +You can initialize the providers without passing the Lucia instance and it does not accept scopes. + +```ts +import { GitHub } from "arctic"; + +export const githubAuth = new GitHub(GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET); +export const googleAuth = new Google( + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + "http://localhost:3000/login/github/callback" +); +``` + +## Create authorization URL + +`createAuthorizationURL()` replaces `getAuthorizationUrl()`. State and code verifier must generated on your side. + +```ts +import { generateState, generateCodeVerifier } from "arctic"; + +// generate state +const state = generateState(); + +// pass state (and code verifier for PKCE) +// returns the authorization url only +const authorizationURL = await githubAuth.createAuthorizationURL(state, { + scopes: ["email"] // pass scopes here instead +}); + +setCookie("github_oauth_state", state, { + secure: true, // set to false in localhost + path: "/", + httpOnly: true, + maxAge: 60 * 10 // 10 min +}); + +// redirect to authorization url +``` + +## Validate callback + +The `state` check stays the same. + +`validateAuthorizationCode()` replaces `validateCallback()`. Instead of returning tokens, users, and database methods, it just returns tokens. Use the access token to get the user, then check if the user is already registered and create a new user if they aren't. + +You now have to create users and manage OAuth accounts by yourself. + +```ts +import { generateId } from "lucia"; + +// check for state +// ... + +// only returns tokens +const tokens = await githubAuth.validateAuthorizationCode(code); + +// use the access token to get the user +const githubUser = await githubAuth.getUser(tokens.accessToken); + +const existingAccount = await db + .table("oauth_account") + .where("provider_id", "=", "github") + .where("provider_user_id", "=", githubUser.id) + .get(); + +if (existingAccount) { + // simplified `createSession()` - second param for session attributes + const session = await lucia.createSession(existingUser.id, {}); + + // `createSessionCookie()` now takes a session ID instead of the entire session object + const sessionCookie = lucia.createSessionCookie(session.id); + + // set session cookie as usual (using `Response` as example) + return new Response(null, { + status: 302, + headers: { + Location: "/", + "Set-Cookie": sessionCookie.serialize() + } + }); +} + +// v2 IDs have a length of 15 +const userId = generateId(15); + +await db.beginTransaction(); +// create user manually +await db.table("user").insert({ + id: userId, + username: github.login +}); +// store oauth account +await db.table("oauth_account").insert({ + provider_id: "github", + provider_user_id: githubUser.id, + user_id: userId +}); +await db.commit(); + +// simplified `createSession()` - second param for session attributes +const session = await lucia.createSession(userId, {}); +// `createSessionCookie()` now takes a session ID instead of the entire session object +const sessionCookie = lucia.createSessionCookie(session.id); +// set session cookie as usual (using `Response` as example) +return new Response(null, { + status: 302, + headers: { + Location: "/", + "Set-Cookie": sessionCookie.serialize() + } +}); +``` + +### Error handling + +Error handling has improved with v3. `validateAuthorizationCode()` throws an `OAuth2RequestError`, which includes proper error messages and descriptions. + +```ts +try { + const tokens = await githubAuth.validateAuthorizationCode(code); + // ... +} catch (e) { + console.log(e); + if (e instanceof OAuth2RequestError) { + // bad verification code, invalid credentials, etc + return new Response(null, { + status: 400 + }); + } + return new Response(null, { + status: 500 + }); +} +``` diff --git a/docs/pages/translations/ja/upgrade-v3/password.md b/docs/pages/translations/ja/upgrade-v3/password.md new file mode 100644 index 000000000..1899d4f17 --- /dev/null +++ b/docs/pages/translations/ja/upgrade-v3/password.md @@ -0,0 +1,77 @@ +--- +title: "Upgrade password-based auth to v3" +--- + +# Upgrade password-based auth to v3 + +## Update database + +You can continue using the keys table but we recommend either creating a dedicated table for storing passwords or storing passwords in the user table, as shown in the database migration guides. + +## Create users + +Lucia provides `LegacyScrypt` for hashing and comparing passwords using the algorithm used in v1 and v2. For future projects, we recommend using `Argon2id` or `Scrypt` provided by Oslo. + +```ts +import { generateId, LegacyScrypt } from "lucia"; + +// v2 IDs have a length of 15 +const userId = generateId(15); + +await db.beginTransaction(); +// create user manually +await db.table("user").insert({ + id: userId, + username +}); +// store oauth account +await db.table("password").insert({ + hashed_password: await new LegacyScrypt().hash(password), + user_id: userId +}); +await db.commit(); + +// simplified `createSession()` - second param for session attributes +const session = await lucia.createSession(userId, {}); +// `createSessionCookie()` now takes a session ID instead of the entire session object +const sessionCookie = lucia.createSessionCookie(session.id); +// set session cookie as usual (using `Response` as example) +return new Response(null, { + status: 302, + headers: { + Location: "/", + "Set-Cookie": sessionCookie.serialize() + } +}); +``` + +## Authenticate users + +Use `verify()` to validate passwords. + +```ts +import { LegacyScrypt } from "lucia"; + +// using consecutive queries to simplify example but you can use joins +const user = await db.table("user").where("username", "=", username).get(); +if (!user) { + return new Response("Invalid username or password", { + status: 400 + }); +} +const credentials = await db.table("password").where("user_id", "=", user.id).get(); +if (!user) { + return new Response("Invalid username or password", { + status: 400 + }); +} + +const validPassword = await new LegacyScrypt().verify(credentials.hashed_password, password); +if (!validPassword) { + return new Response("Invalid username or password", { + status: 400 + }); +} + +// create sessions... +``` diff --git a/docs/pages/translations/ja/upgrade-v3/postgresql.md b/docs/pages/translations/ja/upgrade-v3/postgresql.md new file mode 100644 index 000000000..6fbfbd075 --- /dev/null +++ b/docs/pages/translations/ja/upgrade-v3/postgresql.md @@ -0,0 +1,115 @@ +--- +title: "Upgrade your PostgreSQL database to v3" +--- + +# Upgrade your PostgreSQL database to v3 + +**Migration must be handled manually or else you will lose all your data**. **Do NOT use automated tools as is.** Read this guide carefully as some parts depend on your current structure (**especially the table names**), and feel free to ask questions on our Discord server if you have any questions. + +## Update the adapter + +Install the latest version of the PostgreSQL adapter package. + +``` +npm install @lucia-auth/adapter-postgresql +``` + +Initialize the adapter: + +```ts +import { NodePostgresAdapter, PostgresJsAdapter } from "@lucia-auth/adapter-postgresql"; + +// previously named `pg` adapter +new NodePostgresAdapter(pool, { + // table names + user: "auth_user", + session: "user_session" +}); + +// previously named `postgres` adapter +new PostgresJsAdapter(sql, { + // table names + user: "auth_user", + session: "user_session" +}); +``` + +## Update session table + +The main change to the session table is that the `idle_expires` and `active_expires` columns are replaced with a single `expires_at` column. Unlike the previous columns, it's a `DATETIME` column. + +**Check your table names before running the code.** + +```sql +START TRANSACTION; + +ALTER TABLE user_session ADD COLUMN expires_at TIMESTAMPTZ; + +UPDATE user_session SET expires_at = to_timestamp(idle_expires / 1000); + +ALTER TABLE user_session +DROP COLUMN active_expires, +DROP COLUMN idle_expires, +ALTER COLUMN expires_at SET NOT NULL; +``` + +Do a final check and commit the transaction. + +```sql +COMMIT; +``` + +You may also just delete the session table and replace it with the [new schema](/database/postgresql#schema). + +## Replace key table + +You can keep using the key table, but we recommend using dedicated tables for each authentication method. + +### OAuth + +The SQL below creates a dedicated table `oauth_account` for storing all user OAuth accounts. This assumes all keys where `hashed_password` column is null are for OAuth accounts. You may also separate them by the OAuth provider. + +```sql +CREATE TABLE oauth_account ( + provider_id TEXT NOT NULL, + provider_user_id TEXT NOT NULL, + user_id TEXT NOT NULL REFERENCES auth_user(id), + PRIMARY KEY (provider_id, provider_user_id) +); + +INSERT INTO oauth_account (provider_id, provider_user_id, user_id) +SELECT SUBSTRING(id, 1, POSITION(':' IN id)-1), SUBSTRING(id, POSITION(':' IN id)+1), user_id FROM user_key +WHERE hashed_password IS NULL; +``` + +### Email/password + +The SQL below creates a dedicated table `password` for storing user passwords. This assumes the provider ID for emails was `email` and that you're already storing the users' emails in the user table. + +```sql +CREATE TABLE password ( + id SERIAL PRIMARY KEY, + hashed_password TEXT NOT NULL, + user_id TEXT NOT NULL REFERENCES auth_user(id) +); + +INSERT INTO password (hashed_password, user_id) +SELECT hashed_password, user_id FROM user_key +WHERE SUBSTRING(id, 1, POSITION(':' IN id)-1) = 'email'; +``` + +Alternatively, you can store the user's credentials in the user table if you only work with email/password. + +```sql +START TRANSACTION; + +ALTER TABLE auth_user ADD COLUMN hashed_password TEXT; + +UPDATE auth_user SET hashed_password = user_key.hashed_password FROM user_key +WHERE user_key.user_id = auth_user.id +AND user_key.hashed_password IS NOT NULL; + +ALTER TABLE auth_user ALTER COLUMN hashed_password SET NOT NULL; + +COMMIT; +``` diff --git a/docs/pages/translations/ja/upgrade-v3/prisma/index.md b/docs/pages/translations/ja/upgrade-v3/prisma/index.md new file mode 100644 index 000000000..7250c1d72 --- /dev/null +++ b/docs/pages/translations/ja/upgrade-v3/prisma/index.md @@ -0,0 +1,30 @@ +--- +title: "Upgrade your Prisma project to v3" +--- + +# Upgrade your Prisma project to v3 + +## Update the adapter + +Install the latest version of the Prisma adapter. + +``` +npm install @lucia-auth/adapter-prisma +``` + +Initialize the adapter: + +```ts +import { PrismaClient } from "@prisma/client"; +import { PrismaAdapter } from "@lucia-auth/adapter-prisma"; + +const client = new PrismaClient(); + +new PrismaAdapter(client.session, client.user); +``` + +## Update schema and database + +- [MySQL](/upgrade-v3/prisma/mysql) +- [PostgreSQL](/upgrade-v3/prisma/postgresql) +- [SQLite](/upgrade-v3/prisma/sqlite) diff --git a/docs/pages/translations/ja/upgrade-v3/prisma/mysql.md b/docs/pages/translations/ja/upgrade-v3/prisma/mysql.md new file mode 100644 index 000000000..eec21d1cf --- /dev/null +++ b/docs/pages/translations/ja/upgrade-v3/prisma/mysql.md @@ -0,0 +1,125 @@ +--- +title: "Upgrade Prisma and your MySQL database to v3" +--- + +# Upgrade Prisma and your MySQL database to v3 + +The v3 Prisma adapter now requires all fields to be `camelCase`. + +**Migration must be handled manually or else you will lose all your data**. **Do NOT use Prisma's migration tools as is**. Read this guide carefully as some parts depend on your current structure (**especially the table names**), and feel free to ask questions on our Discord server if you have any questions. + +## Update session table + +The main change to the session table is that the `idle_expires` and `active_expires` fields are replaced with a single `expiresAt` field. Unlike the previous columns, it's a `DateTime` type. Update the `Session` model. Make sure to add any custom attributes you previously had. + +```prisma +model Session { + id String @id + userId String + expiresAt DateTime + user User @relation(references: [id], fields: [userId], onDelete: Cascade) +} +``` + +If you're fine with clearing your session table, you can now migrate your database and you're done updating it. + +However, if you'd like to keep your session data, first run `prisma migrate` **with the `--create-only` flag.** + +``` +npx prisma migrate dev --name updated_session --create-only +``` + +Find the migration file inside `prisma/migrations/X_updated_session` and replace it with the SQL below. Make sure to alter it if you have custom session attributes. + +**This script assumes your session and user models are named `Session` and `User`.** + +```sql +ALTER TABLE `Session` ADD `expiresAt` DATETIME(3), DROP FOREIGN KEY `Session_user_id_fkey`; + +UPDATE `Session` SET `expiresAt` = FROM_UNIXTIME(`idle_expires` / 1000); + +ALTER TABLE `Session` +DROP `active_expires`, +DROP `idle_expires`, +RENAME COLUMN `user_id` TO `userId`, +MODIFY `expiresAt` DATETIME(3) NOT NULL, +ADD CONSTRAINT `Session_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; +``` + +Finally, run the migration: + +``` +npx prisma migrate dev --name updated_session +``` + +## Replace key table + +You can keep using the key table, but we recommend using dedicated tables for each authentication method. + +### OAuth + +This creates a dedicated model for user OAuth accounts. + +```prisma +model User { + id String @id + sessions Session[] + oauthAccounts OauthAccount[] +} + +model OauthAccount { + providerId String + providerUserId String + userId String + user User @relation(references: [id], fields: [userId], onDelete: Cascade) + + @@id([providerId, providerUserId]) +} +``` + +Update your database: + +``` +npx prisma migrate dev --name added_oauth_account_table +``` + +Finally, copy the data from the key table. This assumes all keys where `hashed_password` column is null are for OAuth accounts. + +```sql +INSERT INTO `OauthAccount` (`providerId`, `providerUserId`, `userId`) +SELECT SUBSTRING(`id`, 1, POSITION(':' IN `id`)-1), SUBSTRING(`id`, POSITION(':' IN `id`)+1), `user_id` FROM `Key` +WHERE `hashed_password` IS NULL; +``` + +### Email/password + +This creates a dedicated model for user passwords. + +```prisma +model User { + id String @id + sessions Session[] + passwords OauthAccount[] +} + +model Password { + id Int @id @default(autoincrement()) + hashedPassword String + userId String + user User @relation(references: [id], fields: [userId], onDelete: Cascade) +} +``` + +Update your database: + +``` +npx prisma migrate dev --name added_password_table +``` + +Finally, copy the data from the key table. This assumes the provider ID for emails was `email` and that you're already storing the users' emails in the user table. + +```sql +INSERT INTO Password (`hashedPassword`, `userId`) +SELECT `hashed_password`, `user_id` FROM `Key` +WHERE SUBSTRING(`id`, 1, POSITION(':' IN `id`)-1) = 'email'; +``` diff --git a/docs/pages/translations/ja/upgrade-v3/prisma/postgresql.md b/docs/pages/translations/ja/upgrade-v3/prisma/postgresql.md new file mode 100644 index 000000000..1adc2ec39 --- /dev/null +++ b/docs/pages/translations/ja/upgrade-v3/prisma/postgresql.md @@ -0,0 +1,128 @@ +--- +title: "Upgrade Prisma and your PostgreSQL database to v3" +--- + +# Upgrade Prisma and your PostgreSQL database to v3 + +The v3 Prisma adapter now requires all fields to be `camelCase`. + +**Migration must be handled manually or else you will lose all your data**. **Do NOT use Prisma's migration tools as is**. Read this guide carefully as some parts depend on your current structure (**especially the table names**), and feel free to ask questions on our Discord server if you have any questions. + +## Update session table + +The main change to the session table is that the `idle_expires` and `active_expires` fields are replaced with a single `expiresAt` field. Unlike the previous columns, it's a `DateTime` type. Update the `Session` model. Make sure to add any custom attributes you previously had. + +```prisma +model Session { + id String @id + userId String + expiresAt DateTime + user User @relation(references: [id], fields: [userId], onDelete: Cascade) +} +``` + +If you're fine with clearing your session table, you can now migrate your database and you're done updating it. + +However, if you'd like to keep your session data, first run `prisma migrate` **with the `--create-only` flag.** + +``` +npx prisma migrate dev --name updated_session --create-only +``` + +Find the migration file inside `prisma/migrations/X_updated_session` and replace it with the SQL below. Make sure to alter it if you have custom session attributes. + +**This script assumes your session and user models are named `Session` and `User`.** + +```sql +ALTER TABLE "Session" +DROP CONSTRAINT "Session_user_id_fkey", +ADD COLUMN "expiresAt" TIMESTAMP(3); + +UPDATE "Session" SET "expiresAt" = to_timestamp("idle_expires" / 1000); + +ALTER TABLE "Session" RENAME COLUMN "user_id" TO "userId"; + +ALTER TABLE "Session" +DROP COLUMN "active_expires", +DROP COLUMN "idle_expires", +ALTER COLUMN "expiresAt" SET NOT NULL, +ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; +``` + +Finally, run the migration: + +``` +npx prisma migrate dev --name updated_session +``` + +## Replace key table + +You can keep using the key table, but we recommend using dedicated tables for each authentication method. + +### OAuth + +This creates a dedicated model for user OAuth accounts. + +```prisma +model User { + id String @id + sessions Session[] + oauthAccounts OauthAccount[] +} + +model OauthAccount { + providerId String + providerUserId String + userId String + user User @relation(references: [id], fields: [userId], onDelete: Cascade) + + @@id([providerId, providerUserId]) +} +``` + +Update your database: + +``` +npx prisma migrate dev --name added_oauth_account_table +``` + +Finally, copy the data from the key table. This assumes all keys where `hashed_password` column is null are for OAuth accounts. + +```sql +INSERT INTO "OauthAccount" ("providerId", "providerUserId", "userId") +SELECT SUBSTRING("id", 1, POSITION(':' IN "id")-1), SUBSTRING("id", POSITION(':' IN id)+1), "user_id" FROM "Key" +WHERE "hashed_password" IS NULL; +``` + +### Email/password + +This creates a dedicated model for user passwords. + +```prisma +model User { + id String @id + sessions Session[] + passwords OauthAccount[] +} + +model Password { + id Int @id @default(autoincrement()) + hashedPassword String + userId String + user User @relation(references: [id], fields: [userId], onDelete: Cascade) +} +``` + +Update your database: + +``` +npx prisma migrate dev --name added_password_table +``` + +Finally, copy the data from the key table. This assumes the provider ID for emails was `email` and that you're already storing the users' emails in the user table. + +```sql +INSERT INTO "Password" ("hashedPassword", "userId") +SELECT "hashed_password", "user_id" FROM "Key" +WHERE SUBSTRING("id", 1, POSITION(':' IN "id")-1) = 'email'; +``` diff --git a/docs/pages/translations/ja/upgrade-v3/prisma/sqlite.md b/docs/pages/translations/ja/upgrade-v3/prisma/sqlite.md new file mode 100644 index 000000000..0f82db946 --- /dev/null +++ b/docs/pages/translations/ja/upgrade-v3/prisma/sqlite.md @@ -0,0 +1,127 @@ +--- +title: "Upgrade Prisma and your SQLite database to v3" +--- + +# Upgrade Prisma and your SQLite database to v3 + +The v3 Prisma adapter now requires all fields to be `camelCase`. + +**Migration must be handled manually or else you will lose all your data**. **Do NOT use Prisma's migration tools as is**. Read this guide carefully as some parts depend on your current structure (**especially the table names**), and feel free to ask questions on our Discord server if you have any questions. + +## Update session table + +The main change to the session table is that the `idle_expires` and `active_expires` fields are replaced with a single `expiresAt` field. Unlike the previous columns, it's a `DateTime` type. Update the `Session` model. Make sure to add any custom attributes you previously had. + +```prisma +model Session { + id String @id + userId String + expiresAt DateTime + user User @relation(references: [id], fields: [userId], onDelete: Cascade) +} +``` + +If you're fine with clearing your session table, you can now migrate your database and you're done updating it. + +However, if you'd like to keep your session data, first run `prisma migrate` **with the `--create-only` flag.** + +``` +npx prisma migrate dev --name updated_session --create-only +``` + +Find the migration file inside `prisma/migrations/X_updated_session` and replace it with the SQL below. Make sure to alter it if you have custom session attributes. + +**This script assumes your session and user models are named `Session` and `User`.** + +```sql +CREATE TABLE "new_Session" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL REFERENCES "User"("id"), + "expiresAt" DATETIME NOT NULL +); + +INSERT INTO "new_Session" ("id", "userId", "expiresAt") +SELECT "id", "user_id", "idle_expires" FROM "Session"; + +DROP TABLE "Session"; + +ALTER TABLE "new_Session" RENAME TO "Session"; +``` + +Finally, run the migration: + +``` +npx prisma migrate dev --name updated_session +``` + +## Replace key table + +You can keep using the key table, but we recommend using dedicated tables for each authentication method. + +### OAuth + +This creates a dedicated model for user OAuth accounts. + +```prisma +model User { + id String @id + sessions Session[] + oauthAccounts OauthAccount[] +} + +model OauthAccount { + providerId String + providerUserId String + userId String + user User @relation(references: [id], fields: [userId], onDelete: Cascade) + + @@id([providerId, providerUserId]) +} +``` + +Update your database: + +``` +npx prisma migrate dev --name added_oauth_account_table +``` + +Finally, copy the data from the key table. This assumes all keys where `hashed_password` column is null are for OAuth accounts. + +```sql +INSERT INTO "OauthAccount" ("providerId", "providerUserId", "userId") +SELECT substr("id", 1, instr("id", ':')-1), substr("id", instr("id", ':')+1), "user_id" FROM "Key" +WHERE "hashed_password" IS NULL; +``` + +### Email/password + +This creates a dedicated model for user passwords. + +```prisma +model User { + id String @id + sessions Session[] + passwords OauthAccount[] +} + +model Password { + id Int @id @default(autoincrement()) + hashedPassword String + userId String + user User @relation(references: [id], fields: [userId], onDelete: Cascade) +} +``` + +Update your database: + +``` +npx prisma migrate dev --name added_password_table +``` + +Finally, copy the data from the key table. This assumes the provider ID for emails was `email` and that you're already storing the users' emails in the user table. + +```sql +INSERT INTO "Password" ("hashedPassword", "userId") +SELECT "hashed_password", "user_id" FROM "Key" +WHERE substr("id", 1, instr("id", ':')-1) = 'email'; +``` diff --git a/docs/pages/translations/ja/upgrade-v3/sqlite.md b/docs/pages/translations/ja/upgrade-v3/sqlite.md new file mode 100644 index 000000000..3c1eeb2ff --- /dev/null +++ b/docs/pages/translations/ja/upgrade-v3/sqlite.md @@ -0,0 +1,145 @@ +--- +title: "Upgrade your SQLite database to v3" +--- + +# Upgrade your SQLite database to v3 + +**Migration must be handled manually or else you will lose all your data**. **Do NOT use automated tools as is.** Read this guide carefully as some parts depend on your current structure (**especially the table names**), and feel free to ask questions on our Discord server if you have any questions. + +## Update the adapter + +Install the latest version of the SQLite adapter package. + +``` +npm install @lucia-auth/adapter-sqlite +``` + +Initialize the adapter: + +```ts +import { + BetterSqlite3Adapter, + CloudflareD1Adapter, + LibSQLAdapter +} from "@lucia-auth/adapter-sqlite"; + +new BetterSqlite3Adapter(db, { + // table names + user: "user", + session: "session" +}); + +new CloudflareD1Adapter(d1, { + // table names + user: "user", + session: "session" +}); + +new LibSQLAdapter(db, { + // table names + user: "user", + session: "session" +}); +``` + +## Update session table + +The main change to the session table is that the `idle_expires` and `active_expires` columns are replaced with a single `expires_at` column. Unlike the previous columns, this takes a UNIX time in _seconds_. + +Make sure to use transactions and add any additional columns in your existing session table when creating the new table and copying the data. + +**Check your table names before running the code.** + +```sql +BEGIN TRANSACTION; + +CREATE TABLE new_session ( + id TEXT NOT NULL PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES user(id), + expires_at INTEGER NOT NULL +); + +INSERT INTO new_session (id, user_id, expires_at) +SELECT id, user_id, idle_expires / 1000 FROM session; + +DROP TABLE session; + +ALTER TABLE new_session RENAME TO session; +``` + +Check your new `session` table looks right. If not run `ROLLBACK` to rollback the transaction. If you're ready, run `COMMIT` to commit the transaction: + +```sql +COMMIT; +``` + +You may also just delete the session table and replace it with the [new schema](/database/sqlite#schema). + +## Replace key table + +You can keep using the key table, but we recommend using dedicated tables for each authentication method. + +### OAuth + +The SQL below creates a dedicated table `oauth_account` for storing all user OAuth accounts. This assumes all keys where `hashed_password` column is null are for OAuth accounts. You may also separate them by the OAuth provider. + +```sql +CREATE TABLE oauth_account ( + provider_id TEXT NOT NULL, + provider_user_id TEXT NOT NULL, + user_id TEXT NOT NULL REFERENCES user(id), + PRIMARY KEY (provider_id, provider_user_id) +); + +INSERT INTO oauth_account (provider_id, provider_user_id, user_id) +SELECT substr(id, 1, instr(id, ':')-1), substr(id, instr(id, ':')+1), user_id FROM key +WHERE hashed_password IS NULL; +``` + +### Email/password + +The SQL below creates a dedicated table `password` for storing user passwords. This assumes the provider ID for emails was `email` and that you're already storing the users' emails in the user table. + +```sql +CREATE TABLE password ( + hashed_password TEXT NOT NULL, + user_id TEXT NOT NULL REFERENCES user(id) +); + +INSERT INTO password (hashed_password, user_id) +SELECT hashed_password, user_id FROM key +WHERE substr(id, 1, instr(id, ':')-1) = 'email'; +``` + +Alternatively, you can store the user's credentials in the user table if you only work with email/password. Unfortunately, since SQLite's `ALTER` statement only supports a limited number of operations, you'd have to recreate tables that reference the user table. + +```sql +BEGIN TRANSACTION; + +CREATE TABLE new_user ( + id TEXT NOT NULL PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + hashed_password TEXT NOT NULL +); + +INSERT INTO new_user (id, email, hashed_password) +SELECT user.id, email, hashed_password FROM user INNER JOIN key ON key.user_id = user.id +WHERE hashed_password IS NOT NULL; + +CREATE TABLE new_session ( + id TEXT NOT NULL PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES user(id), + expires_at INTEGER NOT NULL +); + +INSERT INTO new_session (id, user_id, expires_at) +SELECT id, user_id, expires_at FROM session; + +DROP TABLE session; +DROP TABLE user; + +ALTER TABLE new_user RENAME TO user; +ALTER TABLE new_session RENAME TO session; + +COMMIT; +```