-
-
Notifications
You must be signed in to change notification settings - Fork 485
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add nextjs username password tutorials
- Loading branch information
1 parent
a525d2f
commit 1e052ad
Showing
5 changed files
with
666 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
304 changes: 302 additions & 2 deletions
304
docs/pages/tutorials/username-and-password/nextjs-app.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
``` |
Oops, something went wrong.