Skip to content

Commit

Permalink
add nextjs username password tutorials
Browse files Browse the repository at this point in the history
  • Loading branch information
pilcrowonpaper committed Dec 25, 2023
1 parent a525d2f commit 1e052ad
Show file tree
Hide file tree
Showing 5 changed files with 666 additions and 8 deletions.
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 @@ -16,7 +16,7 @@ import { Lucia } from "lucia";
export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: PROD
secure: import.meta.env.PROD
}
},
getUserAttributes: (attributes) => {
Expand Down Expand Up @@ -92,7 +92,7 @@ export async function POST(context: APIContext): Promise<Response> {
const userId = generateId(15);
const hashedPassword = await new Argon2id().hash(password);

// TODO: check if username is already taken
// TODO: check if username is already used
await db.table("user").insert({
id: userId,
username: username,
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/tutorials/username-and-password/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ title: "Username and password"

# Tutorial: Username and password auth

The tutorials go over how to implement a basic username and password auth and covers 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.
The tutorials go over how to implement a basic username and password auth and covers 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/tree/v3).

- [Astro](/tutorials/username-and-password/astro)
- [Next.js App router](/tutorials/username-and-password/nextjs-app)
Expand Down
304 changes: 302 additions & 2 deletions docs/pages/tutorials/username-and-password/nextjs-app.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,307 @@
---
title: "Username and password in Next.js App Router"
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 setup your database as described in the [Getting started](/getting-started/nextjs-app) page.

## Update database

Add a `username` column (unique) and `password` column to your user table with a type of 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;
}
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 (
<>
<h1>Create an account</h1>
<form action={signup}>
<label htmlFor="username">Username</label>
<input name="username" id="username" />
<br />
<label htmlFor="password">Password</label>
<input type="password" name="password" id="password" />
<br />
<button>Continue</button>
</form>
</>
);
}

async function signup(_: any, formData: FormData): Promise<ActionResult> {}

interface ActionResult {
error: string;
}
```
still working on this one!

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<ActionResult> {
"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 (
<>
<h1>Sign in</h1>
<form action={login}>
<label htmlFor="username">Username</label>
<input name="username" id="username" />
<br />
<label htmlFor="password">Password</label>
<input type="password" name="password" id="password" />
<br />
<button>Continue</button>
</form>
</>
);
}

async function login(_: any, formData: FormData): Promise<ActionResult> {}

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<ActionResult> {
"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) {
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, validates it, and sets 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 <h1>Hi, {user.username}!</h1>;
}
```

## 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 (
<form action={logout}>
<button>Sign out</button>
</form>
);
}

async function logout(): Promise<ActionResult> {
"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");
}
```
Loading

0 comments on commit 1e052ad

Please sign in to comment.