Skip to content

Commit

Permalink
Better ID generation (#1548)
Browse files Browse the repository at this point in the history
  • Loading branch information
pilcrowOnPaper committed Apr 24, 2024
1 parent 5cd282e commit a15a642
Show file tree
Hide file tree
Showing 29 changed files with 121 additions and 53 deletions.
6 changes: 6 additions & 0 deletions .auri/$0g99ie7j.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
package: "lucia" # package name
type: "minor" # "major", "minor", "patch"
---

Add `generateIdFromEntropySize()`
6 changes: 6 additions & 0 deletions .auri/$r44qejjf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
package: "lucia" # package name
type: "patch" # "major", "minor", "patch"
---

Optimize session ID generation
17 changes: 10 additions & 7 deletions docs/pages/basics/users.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,27 @@ interface Session extends UserAttributes {

## 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.
When creating users, you can use [`generateIdFromEntropySize()`](/reference/main/generateIdFromEntropySize) to generate user IDs, which takes the entropy size in bytes. This will generate a cryptographically secure random string consisting of lowercase letters and numbers.

```ts
import { generateId } from "lucia";
import { generateIdFromEntropySize } from "lucia";

await db.createUser({
id: generateId(15)
// 16 characters long
id: generateIdFromEntropySize(10)
});
```

Use Oslo's [`generateRandomString()`](https://oslo.js.org/reference/crypto/generateRandomString) if you're looking for a more customizable option.
Use Lucia's [`generateId()`](/reference/main/generateIdFromEntropySize) or Oslo's [`generateRandomString()`](https://oslo.js.org/reference/crypto/generateRandomString) if you're looking for a more customizable option.

```ts
import { generateId } from "lucia";

const id = generateId(15);

import { generateRandomString, alphabet } from "oslo/crypto";

await db.createUser({
id: generateRandomString(15, alphabet("a-z", "A-Z", "0-9"))
});
const id = generateRandomString(15, alphabet("a-z", "A-Z", "0-9"));
```

## Define user attributes
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/guides/email-and-password/2fa.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ When the user signs up, set `two_factor_secret` to `null` to indicate the user h
app.post("/signup", async () => {
// ...

const userId = generateId(15);
const userId = generateIdFromEntropySize(10);

await db.table("user").insert({
id: userId,
Expand Down
5 changes: 2 additions & 3 deletions docs/pages/guides/email-and-password/basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Create a `/signup` route. This will accept POST requests with an email and passw

```ts
import { lucia } from "./auth.js";
import { generateId } from "lucia";
import { generateIdFromEntropySize } from "lucia";
import { Argon2id } from "oslo/password";

app.post("/signup", async (request: Request) => {
Expand All @@ -80,7 +80,7 @@ app.post("/signup", async (request: Request) => {
}

const hashedPassword = await new Argon2id().hash(password);
const userId = generateId(15);
const userId = generateIdFromEntropySize(10); // 16 characters long

try {
await db.table("user").insert({
Expand Down Expand Up @@ -127,7 +127,6 @@ Create a `/login` route. This will accept POST requests with an email and passwo

```ts
import { lucia } from "./auth.js";
import { generateId } from "lucia";
import { Argon2id } from "oslo/password";

app.post("/login", async (request: Request) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,12 @@ 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 { generateIdFromEntropySize } from "lucia";

app.post("/signup", async () => {
// ...

const userId = generateId(15);
const userId = generateIdFromEntropySize(10); // 16 characters long

await db.table("user").insert({
id: userId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ import { TimeSpan, createDate } from "oslo";
async function createEmailVerificationToken(userId: string, email: string): Promise<string> {
// optionally invalidate all existing tokens
await db.table("email_verification_token").where("user_id", "=", userId).deleteAll();
const tokenId = generateId(40);
const tokenId = generateIdFromEntropySize(25); // 40 characters long
await db.table("email_verification_token").insert({
id: tokenId,
user_id: userId,
Expand All @@ -81,12 +81,12 @@ async function createEmailVerificationToken(userId: string, email: string): Prom
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 { generateIdFromEntropySize } from "lucia";

app.post("/signup", async () => {
// ...

const userId = generateId(15);
const userId = generateIdFromEntropySize(10); // 16 characters long

await db.table("user").insert({
id: userId,
Expand Down
6 changes: 2 additions & 4 deletions docs/pages/guides/email-and-password/password-reset.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ The token should be valid for at most few hours. The token should be hashed befo
import { TimeSpan, createDate } from "oslo";
import { sha256 } from "oslo/crypto";
import { encodeHex } from "oslo/encoding";
import { generateId } from "lucia";
import { generateIdFromEntropySize } from "lucia";

async function createPasswordResetToken(userId: string): Promise<string> {
// optionally invalidate all existing tokens
await db.table("password_reset_token").where("user_id", "=", userId).deleteAll();
const tokenId = generateId(40);
const tokenId = generateIdFromEntropySize(25); // 40 character
const tokenHash = encodeHex(await sha256(new TextEncoder().encode(tokenId)));
await db.table("password_reset_token").insert({
token_hash: tokenHash,
Expand All @@ -45,8 +45,6 @@ async function createPasswordResetToken(userId: string): Promise<string> {
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;

Expand Down
5 changes: 2 additions & 3 deletions docs/pages/guides/oauth/account-linking.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ This guide uses the database schema shown in the [Multiple OAuth providers](/gui
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";

import { generateIdFromEntropySize } from "lucia";
// Make sure you requested for the "user:email" scope.
const tokens = await github.validateAuthorizationCode(code);
const userResponse = await fetch("https://api.github.com/user", {
Expand Down Expand Up @@ -50,7 +49,7 @@ if (existingUser) {
user_id: existingUser.id
});
} else {
const userId = generateId(15);
const userId = generateIdFromEntropySize(10); // 16 characters long
await db.beginTransaction();
await db.table("user").insert({
id: userId,
Expand Down
4 changes: 2 additions & 2 deletions docs/pages/guides/oauth/basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ In the callback route, first get the state from the cookie and the search params
```ts
import { github, lucia } from "./auth.js";
import { OAuth2RequestError } from "arctic";
import { generateId } from "lucia";
import { generateIdFromEntropySize } from "lucia";
import { parseCookies } from "oslo/cookie";

app.get("/login/github/callback", async (request: Request): Promise<Response> => {
Expand Down Expand Up @@ -149,7 +149,7 @@ app.get("/login/github/callback", async (request: Request): Promise<Response> =>
});
}

const userId = generateId(15);
const userId = generateIdFromEntropySize(10); // 16 characters long
await db.table("user").insert({
id: userId,
username: githubUserResult.login,
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/guides/oauth/multiple-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ if (existingAccount) {
// ...
}

const userId = generateId(15);
const userId = generateIdFromEntropySize(10); // 16 characters long

await db.beginTransaction();
await db.table("user").insert({
Expand Down
2 changes: 2 additions & 0 deletions docs/pages/reference/main/generateId.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ title: "generateId()"

Generates a cryptographically strong random string made of `a-z` (lowercase) and `0-9`.

Unless you have a strict length requirement, use [`generateIdFromEntropySize()`](/reference/main/generateIdFromEntropySize) which provides better performance.

## Definition

```ts
Expand Down
30 changes: 30 additions & 0 deletions docs/pages/reference/main/generateIdFromEntropySize.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
title: "generateIdFromEntropySize()"
---

# `generateIdFromEntropySize()`

Generates a cryptographically strong random string made of `a-z` (lowercase) and `2-7` using the provided entropy size. The output length increases as the entropy size increases.

If `size` is a multiple of 5, the output size will be `(size * 8) / 5` (see base32 encoding).

This has better performance than [`generateId()`](/reference/main/generateId) and should be your default choice.

## Definition

```ts
function generateIdFromEntropySize(size: number): string;
```

### Parameters

- `size`: Number of bytes to use

## Example

```ts
import { generateIdFromEntropySize } from "lucia";

// 16-characters long
generateIdFromEntropySize(10);
```
4 changes: 2 additions & 2 deletions docs/pages/tutorials/github-oauth/astro.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ Create an API route in `pages/login/github/callback.ts` to handle the callback.
// pages/login/github/callback.ts
import { github, lucia } from "@lib/auth";
import { OAuth2RequestError } from "arctic";
import { generateId } from "lucia";
import { generateIdFromEntropySize } from "lucia";

import type { APIContext } from "astro";

Expand Down Expand Up @@ -165,7 +165,7 @@ export async function GET(context: APIContext): Promise<Response> {
return context.redirect("/");
}

const userId = generateId(15);
const userId = generateIdFromEntropySize(10); // 16 characters long

// Replace this with your own DB client.
await db.table("user").insert({
Expand Down
4 changes: 2 additions & 2 deletions docs/pages/tutorials/github-oauth/nextjs-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ Create an API route in `app/login/github/callback/route.ts` to handle the callba
import { github, lucia } from "@/lib/auth";
import { cookies } from "next/headers";
import { OAuth2RequestError } from "arctic";
import { generateId } from "lucia";
import { generateIdFromEntropySize } from "lucia";

export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
Expand Down Expand Up @@ -169,7 +169,7 @@ export async function GET(request: Request): Promise<Response> {
});
}

const userId = generateId(15);
const userId = generateIdFromEntropySize(10); // 16 characters long

// Replace this with your own DB client.
await db.table("user").insert({
Expand Down
4 changes: 2 additions & 2 deletions docs/pages/tutorials/github-oauth/nextjs-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ Create an API route in `pages/api/login/github/callback.ts` to handle the callba
// pages/api/login/github/callback.ts
import { github, lucia } from "@/lib/auth";
import { OAuth2RequestError } from "arctic";
import { generateId } from "lucia";
import { generateIdFromEntropySize } from "lucia";

import type { NextApiRequest, NextApiResponse } from "next";

Expand Down Expand Up @@ -172,7 +172,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
.redirect("/");
}

const userId = generateId(15);
const userId = generateIdFromEntropySize(10); // 16 characters long

// Replace this with your own DB client.
await db.table("user").insert({
Expand Down
4 changes: 2 additions & 2 deletions docs/pages/tutorials/github-oauth/nuxt.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ Create an API route in `server/routes/login/github/callback.get.ts` to handle th
```ts
// server/routes/login/github/callback.get.ts
import { OAuth2RequestError } from "arctic";
import { generateId } from "lucia";
import { generateIdFromEntropySize } from "lucia";

export default defineEventHandler(async (event) => {
const query = getQuery(event);
Expand Down Expand Up @@ -154,7 +154,7 @@ export default defineEventHandler(async (event) => {
return sendRedirect(event, "/");
}

const userId = generateId(15);
const userId = generateIdFromEntropySize(10); // 16 characters long

// Replace this with your own DB client.
await db.table("user").insert({
Expand Down
4 changes: 2 additions & 2 deletions docs/pages/tutorials/github-oauth/sveltekit.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ Create an API route in `routes/login/github/callback/+server.ts` to handle the c
```ts
// routes/login/github/callback/+server.ts
import { OAuth2RequestError } from "arctic";
import { generateId } from "lucia";
import { generateIdFromEntropySize } from "lucia";
import { github, lucia } from "$lib/server/auth";

import type { RequestEvent } from "@sveltejs/kit";
Expand Down Expand Up @@ -166,7 +166,7 @@ export async function GET(event: RequestEvent): Promise<Response> {
...sessionCookie.attributes
});
} else {
const userId = generateId(15);
const userId = generateIdFromEntropySize(10); // 16 characters long

// Replace this with your own DB client.
await db.table("user").insert({
Expand Down
4 changes: 2 additions & 2 deletions docs/pages/tutorials/username-and-password/astro.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Create an API route in `pages/api/signup.ts`. First, do a very basic input valid
```ts
// pages/api/signup.ts
import { lucia } from "@lib/auth";
import { generateId } from "lucia";
import { generateIdFromEntropySize } from "lucia";
import { Argon2id } from "oslo/password";

import type { APIContext } from "astro";
Expand All @@ -104,7 +104,7 @@ export async function POST(context: APIContext): Promise<Response> {
});
}

const userId = generateId(15);
const userId = generateIdFromEntropySize(10); // 16 characters long
const hashedPassword = await new Argon2id().hash(password);

// TODO: check if username is already used
Expand Down
4 changes: 2 additions & 2 deletions docs/pages/tutorials/username-and-password/nextjs-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ import { Argon2id } from "oslo/password";
import { cookies } from "next/headers";
import { lucia } from "@/lib/auth";
import { redirect } from "next/navigation";
import { generateId } from "lucia";
import { generateIdFromEntropySize } from "lucia";

export default async function Page() {}

Expand All @@ -117,7 +117,7 @@ async function signup(_: any, formData: FormData): Promise<ActionResult> {
}

const hashedPassword = await new Argon2id().hash(password);
const userId = generateId(15);
const userId = generateIdFromEntropySize(10); // 16 characters long

// TODO: check if username is already used
await db.table("user").insert({
Expand Down
4 changes: 2 additions & 2 deletions docs/pages/tutorials/username-and-password/nextjs-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ Create an API route in `pages/api/signup.ts`. First, do a very basic input valid
```ts
// pages/api/signup.ts
import { lucia } from "@/lib/auth";
import { generateId } from "lucia";
import { generateIdFromEntropySize } from "lucia";
import { Argon2id } from "oslo/password";

import type { NextApiRequest, NextApiResponse } from "next";
Expand Down Expand Up @@ -134,7 +134,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}

const hashedPassword = await new Argon2id().hash(password);
const userId = generateId(15);
const userId = generateIdFromEntropySize(10); // 16 characters long

// TODO: check if username is already used
await db.table("user").insert({
Expand Down
4 changes: 2 additions & 2 deletions docs/pages/tutorials/username-and-password/nuxt.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Create an API route in `server/api/signup.post.ts`. First, do a very basic input
```ts
// server/api/signup.post.ts
import { Argon2id } from "oslo/password";
import { generateId } from "lucia";
import { generateIdFromEntropySize } from "lucia";
import { SqliteError } from "better-sqlite3";

export default eventHandler(async (event) => {
Expand All @@ -114,7 +114,7 @@ export default eventHandler(async (event) => {
}

const hashedPassword = await new Argon2id().hash(password);
const userId = generateId(15);
const userId = generateIdFromEntropySize(10); // 16 characters long

// TODO: check if username is already used
await db.table("user").insert({
Expand Down
Loading

0 comments on commit a15a642

Please sign in to comment.