diff --git a/docs/02-app/01-building-your-application/09-authentication/index.mdx b/docs/02-app/01-building-your-application/09-authentication/index.mdx index fdf1611521551..ccaae9979faf1 100644 --- a/docs/02-app/01-building-your-application/09-authentication/index.mdx +++ b/docs/02-app/01-building-your-application/09-authentication/index.mdx @@ -1,38 +1,447 @@ --- title: Authentication -description: Learn how to implement authentication in Next.js, covering best practices, securing routes, authorization techniques, and session management. +description: Learn how to implement authentication in your Next.js application. --- -To implement authentication in Next.js, familiarize yourself with three foundational concepts: +Understanding authentication is crucial for protecting your application's data. This page will guide you through how to use Next.js features to implement auth, as well as patterns so you can choose the right strategy for your application. -- **[Authentication](#authentication)** verifies if the user is who they say they are. It requires the user to prove their identity with something they have, such as a username and password. -- **[Session Management](#session-management)** tracks the user's state (e.g. logged in) across multiple requests. -- **[Authorization](#authorization)** decides what parts of the application the user is allowed to access. +Before starting, it helps to break down the process into three concepts: -This page demonstrates how to use Next.js features to implement common authentication, authorization, and session management patterns so you can choose the best solutions based on your application's needs. +1. **[Authentication](#authentication)**: Verifies if the user is who they say they are. It requires the user to prove their identity with something they have, such as a username and password. +2. **[Session Management](#session-management)**: Tracks the user's auth state across requests. +3. **[Authorization](#authorization)**: Decides what routes and data the user can access. + +This diagram shows the auth flow with React and Next.js features: + +Diagram showing the authentication flow with React and Next.js features + +The examples on this page will walk you through basic username and password auth for educational purposes. While you can implement a custom auth solution, for increased security and simplicity, we recommend using an authentication library. These offer built-in solutions for authentication, session management, and authorization, as well as additional features such as social logins, multi-factor authentication, and role-based access control. You can find a list in the [Auth Libraries](#auth-libraries) section. ## Authentication -Authentication verifies a user's identity. This happens when a user logs in, either with a username and password or through a service like Google. It's all about confirming that users are really who they claim to be, protecting both the user's data and the application from unauthorized access or fraudulent activities. + + +### Sign-up and login functionality + +You can use the [`
`](https://react.dev/reference/react-dom/components/form) element with React's [Server Actions](/docs/app/building-your-application/rendering/server-components), [`useFormStatus()`](https://react.dev/reference/react-dom/hooks/useFormStatus), and [`useFormState()`](https://react.dev/reference/react-dom/hooks/useFormState), to capture user credentials, validate form fields, and call your Authentication Provider's API or database. Since Server Actions always execute on the server, they provide a secure environment for handling authentication logic. + +Here are the steps to implement signup/login functionality: + +#### 1. Capture user credentials + +To capture user credentials, create a form that invokes a Server Action on submission. For example, a signup form that accepts the user's name, email, and password: + +```tsx filename="app/ui/signup-form.tsx" switcher +import { signup } from '@/app/actions/auth' + +export function SignupForm() { + return ( + +
+ + +
+
+ + +
+
+ + +
+ +
+ ) +} +``` + +```jsx filename="app/ui/signup-form.js" switcher +import { signup } from '@/app/actions/auth' + +export function SignupForm() { + return ( +
+
+ + +
+
+ + +
+
+ + +
+ +
+ ) +} +``` + +```tsx filename="app/actions/auth.tsx" switcher +export async function signup(formData: FormData) {} +``` + +```jsx filename="app/actions/auth.js" switcher +export async function signup(formData) {} +``` + +#### 2. Validate form fields on the server + +Use the Server Action to validate the form fields on the server. If your authentication provider doesn't provide form validation, you can use a schema validation library like [Zod](https://zod.dev/) or [Yup](https://github.com/jquense/yup). + +Normal validation practices apply here. For example, you should check that the user has entered a valid email address, a password that meets your security requirements, name is not empty, etc. Using Zod as an example, you can define a form schema with appropriate error messages: + +```ts filename="app/lib/definitions.ts" switcher +import { z } from 'zod' + +export const SignupFormSchema = z.object({ + name: z + .string() + .min(2, { message: 'Name must be at least 2 characters long.' }) + .trim(), + email: z.string().email({ message: 'Please enter a valid email.' }).trim(), + password: z + .string() + .min(8, { message: 'Be at least 8 characters long' }) + .regex(/[a-zA-Z]/, { message: 'Contain at least one letter.' }) + .regex(/[0-9]/, { message: 'Contain at least one number.' }) + .regex(/[^a-zA-Z0-9]/, { + message: 'Contain at least one special character.', + }) + .trim(), +}) + +export type FormState = + | { + errors?: { + name?: string[] + email?: string[] + password?: string[] + } + message?: string + } + | undefined +``` + +```js filename="app/lib/definitions.js" switcher +import { z } from 'zod' + +export const SignupFormSchema = z.object({ + name: z + .string() + .min(2, { message: 'Name must be at least 2 characters long.' }) + .trim(), + email: z.string().email({ message: 'Please enter a valid email.' }).trim(), + password: z + .string() + .min(8, { message: 'Be at least 8 characters long' }) + .regex(/[a-zA-Z]/, { message: 'Contain at least one letter.' }) + .regex(/[0-9]/, { message: 'Contain at least one number.' }) + .regex(/[^a-zA-Z0-9]/, { + message: 'Contain at least one special character.', + }) + .trim(), +}) +``` + +After validation, `return` early if any form fields do not match the schema to prevent unnecessary calls to your authentication provider's API or database: + +```ts filename="app/actions/auth.ts" switcher +import { SignupFormSchema, FormState } from '@/app/lib/definitions' + +export async function signup(state: FormState, formData: FormData) { + // Validate form fields + const validatedFields = SignupFormSchema.safeParse({ + name: formData.get('name'), + email: formData.get('email'), + password: formData.get('password'), + }) + + // If any form fields are invalid, return early + if (!validatedFields.success) { + return { + errors: validatedFields.error.flatten().fieldErrors, + } + } + + // Call the provider or db to create a user... +} +``` + +```js filename="app/actions/auth.js" switcher +import { SignupFormSchema } from '@/app/lib/definitions' + +export async function signup(formData: FormData) { + // Validate form fields + const validatedFields = SignupFormSchema.safeParse({ + name: formData.get('name'), + email: formData.get('email'), + password: formData.get('password'), + }) + + // If any form fields are invalid, return early + if (!validatedFields.success) { + return { + errors: validatedFields.error.flatten().fieldErrors, + } + } + + // Call the provider or db to create a user... +} +``` + +Back in your ``, you can use React's `useFormState()` hook to display validation errors to the user: + +```tsx filename="app/ui/signup-form.tsx" switcher highlight={7,15,21,27-36} +'use client' + +import { useFormState } from 'react-dom' +import { signup } from '@/app/actions/auth' + +export function SignupForm() { + const [state, action] = useFormState(signup, undefined) + + return ( +
+
+ + +
+ {state?.errors?.name &&

{state.errors.name}

} + +
+ + +
+ {state?.errors?.email &&

{state.errors.email}

} + +
+ + +
+ {state?.errors?.password && ( +
+

Password must:

+
    + {state.errors.password.map((error) => ( +
  • - {error}
  • + ))} +
+
+ )} + + + ) +} +``` + +```jsx filename="app/ui/signup-form.js" switcher highlight={7,15,21,27-36} +'use client' + +import { useFormState } from 'react-dom' +import { signup } from '@/app/actions/auth' + +export function SignupForm() { + const [state, action] = useFormState(signup, undefined) + + return ( +
+
+ + +
+ {state.errors.name &&

{state.errors.name}

} + +
+ + +
+ {state.errors.email &&

{state.errors.email}

} + +
+ + +
+ {state.errors.password && ( +
+

Password must:

+
    + {state.errors.password.map((error) => ( +
  • - {error}
  • + ))} +
+
+ )} + + + ) +} +``` + +You can also use the `useFormStatus()` hook to handle the pending state on form submission: + +```tsx filename="app/ui/signup-form.tsx" switcher +'use client' + +import { useFormStatus, useFormState } from 'react-dom' + +// ... + +export function SignupButton() { + const { pending } = useFormStatus() + + return ( + + ) +} +``` + +```jsx filename="app/ui/signup-form.js" switcher +'use client' + +import { useFormStatus, useFormState } from 'react-dom' + +// ... + +export function SignupButton() { + const { pending } = useFormStatus() + + return ( + + ) +} +``` + +> **Tip:** `useFormStatus()` must be called from a component that is rendered inside a `
`. See the [React Docs](https://react.dev/reference/react-dom/hooks/useFormStatus#usage) for more information. + +#### 3. Create a user or check user credentials + +After validation, you can create a new user account or check if the user exists by calling your authentication provider's API or database. + +Continuing from the previous example, ensure the user's email is unique and passwords are stored securely: + +```tsx filename="app/actions/auth.tsx" switcher +export async function signup(state: FormState, formData: FormData) { + // 1. Validate form fields + // ... + + // 2. Prepare data for insertion into database + const { name, email, password } = validatedFields.data + + // Check if the user's email already exists + const existingUser = await db.query.users.findFirst({ + where: eq(users.email, email), + }) + + // If the user already exists, return early + if (existingUser) { + return { + message: 'Email already exists, please login or use a different email.', + } + } + + // Hash the user's password before storing it + const hashedPassword = await bcrypt.hash(password, 10) + + // 3. Insert the user into the database or call an Auth Library's API + const data = await db + .insert(users) + .values({ + name, + email, + password: hashedPassword, + }) + // Only return the user information you need to store in the session + .returning({ id: users.id }) + + const user = data[0] + + if (!user) { + return { + message: 'An error occurred while creating your account.', + } + } + + // TODO: + // 4. Create user session + // 5. Redirect user +} +``` + +```jsx filename="app/actions/auth.js" switcher +export async function signup(state, formData) { + // 1. Validate form fields + // ... -### Authentication Strategies + // 2. Prepare data for insertion into database + const { name, email, password } = validatedFields.data -Modern web applications commonly use several authentication strategies: + // Check if the user's email already exists + const existingUser = await db.query.users.findFirst({ + where: eq(users.email, email), + }) + + // If the user already exists, return early + if (existingUser) { + return { + message: 'Email already exists, please login or use a different email.', + } + } + + // Hash the user's password before storing it + const hashedPassword = await bcrypt.hash(password, 10) + + // 3. Insert the user into the database or call an Library API + const data = await db + .insert(users) + .values({ + name, + email, + password: hashedPassword, + }) + // Only return the user information you need to store in the session + .returning({ id: users.id }) + + const user = data[0] -1. **OAuth/OpenID Connect (OIDC)**: Enable third-party access without sharing user credentials. Ideal for social media logins and Single Sign-On (SSO) solutions. They add an identity layer with OpenID Connect. -2. **Credentials-based login (Email + Password)**: A standard choice for web applications, where users log in with an email and password. Familiar and easy to implement, it requires robust security measures against threats like phishing. -3. **Passwordless/Token-based authentication**: Use email magic links or SMS one-time codes for secure, password-free access. Popular for its convenience and enhanced security, this method helps reduce password fatigue. Its limitation is the dependency on the user's email or phone availability. -4. **Passkeys/WebAuthn**: Use cryptographic credentials unique to each site, offering high security against phishing. Secure but new, this strategy can be difficult to implement. + if (!user) { + return { + message: 'An error occurred while creating your account.', + } + } + + // TODO: + // 4. Create user session + // 5. Redirect user +} +``` -Selecting an authentication strategy should align with your application's specific requirements, user interface considerations, and security objectives. +After successfully creating the user account or verifying the user, you can create a session to manage the user's auth state. Depending on your session management strategy, the session can be stored in a cookie or database, or both. Continue to the [Session Management](#session-management) section to learn more. -### Implementing Authentication +> **Tips:** +> +> - The example above is verbose since it breaks down the authentication steps for the purpose of education. This highlights that implementing your own secure solution can quickly become complex. Consider using an [Auth Library](#auth-libraries) to simplify the process. +> - To improve the user experience, you may want to check for duplicate emails or usernames earlier in the authentication flow. For example, as the user types in their email or the input field loses focus. This can help prevent unnecessary form submissions and provide immediate feedback to the user. You can debounce requests with libraries such as [use-debounce](https://www.npmjs.com/package/use-debounce) to manage the frequency of these checks. -In this section, we'll explore the process of adding basic email-password authentication to a web application. While this method provides a fundamental level of security, it's worth considering more advanced options like OAuth or passwordless logins for enhanced protection against common security threats. The authentication flow we'll discuss is as follows: + -1. The user submits their credentials through a login form. +Here are the steps to implement a sign-up and/or login form: + +1. The user submits their credentials through a form. 2. The form sends a request that is handled by an API route. 3. Upon successful verification, the process is completed, indicating the user's successful authentication. 4. If verification is unsuccessful, an error message is shown. @@ -161,730 +570,1143 @@ export default async function handler(req, res) { +## Session Management + +Session management ensures that the user's authenticated state is preserved across requests and sometimes devices. It involves creating, storing, refreshing, and deleting sessions or tokens. + +There are two types of sessions: + +1. **Stateless**: Session data (or a token) is stored in the browser's cookies. The cookie is sent with each request, allowing the session to be verified on the server. This method is simpler, but can be less secure if not implemented correctly. +2. **Database**: Session data is stored in a database, with the user's browser only receiving the encrypted session ID. This method is more secure, but can be complex and use more server resources. + +> While you can use either method, or both, we recommend using session management library such as [iron-session](https://github.com/vvo/iron-session) or [Jose](https://github.com/panva/jose). + +### Stateless Sessions + -1. The user submits their credentials through a login form. -2. The form calls a Server Action. -3. Upon successful verification, the process is completed, indicating the user's successful authentication. -4. If verification is unsuccessful, an error message is shown. +To create and manage stateless sessions in your Next.js application, there are a few steps you need to follow: -Consider a login form where users can input their credentials: +1. Generate a secret key, which will be used to sign your session, and store it as an [environment variable](/docs/app/building-your-application/configuring/environment-variables). +2. Write logic to encrypt/decrypt session data using a session management library. +3. Save the session as a cookie using the Next.js [`cookies()`](/docs/app/api-reference/functions/cookies) API. The cookie should be set on the server, and include the [recommended options](#3-setting-the-cookie-recommended-options). -```tsx filename="app/login/page.tsx" switcher -import { authenticate } from '@/app/lib/actions' +In addition to the above, consider adding functionality to [update (or refreshs)](#updating-or-extending-the-session) the session when the user returns to the application, and [delete](#deleting-the-session) the session when the user logs out. -export default function Page() { - return ( - - - - - - ) -} +> **Tips:** +> +> - If you're using an [auth library](#auth-libraries), check if they handle sessions. + +#### 1. Generating a secret key + +There are a few ways you can generate secret key to sign your session. For example, you may choose to use the `openssl` command in your terminal: + +```bash filename="terminal" +openssl rand -base64 32 ``` -```jsx filename="app/login/page.jsx" switcher -import { authenticate } from '@/app/lib/actions' +This command generates a 32-character random string that you can use as your secret key and store in your [environment variables file](/docs/app/building-your-application/configuring/environment-variables): -export default function Page() { - return ( -
- - - -
- ) -} +```bash filename=".env" +SESSION_SECRET=your_secret_key ``` -The form above has two input fields for capturing the user's email and password. On submission, it calls the `authenticate` Server Action. +You can then reference this key in your session management logic: -You can then call your Authentication Provider's API in the Server Action to handle authentication: +```js filename="app/actions/session.js" +const secretKey = process.env.SESSION_SECRET +``` -```ts filename="app/lib/actions.ts" switcher -'use server' +#### 2. Encrypting and decrypting sessions -import { signIn } from '@/auth' +Next, you can use your preferred [session management library](#session-management-libraries) to encrypt and decrypt sessions. Continuing from the previous example, we'll use [Jose](https://www.npmjs.com/package/jose) (compatible with the [Edge Runtime](/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes)): + +```tsx filename="app/actions/session.ts" switcher +import 'server-only' +import { SignJWT, jwtVerify } from 'jose' +import { SessionPayload } from '@/app/lib/definitions' + +const secretKey = process.env.SESSION_SECRET +const encodedKey = new TextEncoder().encode(secretKey) -export async function authenticate(_currentState: unknown, formData: FormData) { +export async function encrypt(payload: SessionPayload) { + return new SignJWT(payload) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('7d') + .sign(encodedKey) +} + +export async function decrypt(session: string | undefined = '') { try { - await signIn('credentials', formData) + const { payload } = await jwtVerify(session, encodedKey, { + algorithms: ['HS256'], + }) + return payload } catch (error) { - if (error) { - switch (error.type) { - case 'CredentialsSignin': - return 'Invalid credentials.' - default: - return 'Something went wrong.' - } - } - throw error + console.log('Failed to verify session') } } ``` -```js filename="app/lib/actions.js" switcher -'use server' +```jsx filename="app/actions/session.js" switcher +import 'server-only' +import { SignJWT, jwtVerify } from 'jose' -import { signIn } from '@/auth' +const secretKey = process.env.SESSION_SECRET +const encodedKey = new TextEncoder().encode(secretKey) -export async function authenticate(_currentState, formData) { +export async function encrypt(payload) { + return new SignJWT(payload) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('7d') + .sign(encodedKey) +} + +export async function decrypt(session) { try { - await signIn('credentials', formData) + const { payload } = await jwtVerify(session, encodedKey, { + algorithms: ['HS256'], + }) + return payload } catch (error) { - if (error) { - switch (error.type) { - case 'CredentialsSignin': - return 'Invalid credentials.' - default: - return 'Something went wrong.' - } - } - throw error + console.log('Failed to verify session') } } ``` -
+Above, we're also using React's [`server-only`](https://www.npmjs.com/package/server-only) package to ensure that your session management logic is only executed on the server. -In this code, the `signIn` method checks the credentials against stored user data. -After the authentication provider processes the credentials, there are two possible outcomes: +> **Tips**: +> +> - The payload should contain the **minimum**, unique user data that'll be used in subsequent requests, such as the user's ID, role, etc. It should not contain personally identifiable information like phone number, email address, credit card information, etc, or sensitive data like passwords. -- **Successful Authentication**: This outcome implies that the login was successful. Further actions, such as accessing protected routes and fetching user information, can then be initiated. -- **Failed Authentication**: In cases where the credentials are incorrect or an error is encountered, the function returns a corresponding error message to indicate the authentication failure. +#### 3. Setting the cookie (recommended options) - +To store the session in a cookie, use the Next.js [`cookies()`](/docs/app/api-reference/functions/cookies) API. The cookie should be set on the server, and include the recommended options: -Finally, in your `login-form.tsx` component, you can use React's `useFormState` to call the Server Action and handle form errors, and use `useFormStatus` to handle the pending state of the form: +- **HttpOnly**: Prevents client-side JavaScript from accessing the cookie. +- **Secure**: Use https to send the cookie. +- **SameSite**: Specify whether the cookie can be sent with cross-site requests. +- **Max-Age or Expires**: Delete the cookie after a certain period. +- **Path**: Define the URL path for the cookie. -```tsx filename="app/login/page.tsx" switcher -'use client' +Please refer to [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) for more information on each of these options. -import { authenticate } from '@/app/lib/actions' -import { useFormState, useFormStatus } from 'react-dom' +Continuing from the previous example, here's how you'd save the encrypted session as a cookie: -export default function Page() { - const [errorMessage, dispatch] = useFormState(authenticate, undefined) +```ts filename="app/actions/session.ts" switcher +import 'server-only' +import { cookies } from 'next/headers' - return ( -
- - -
{errorMessage &&

{errorMessage}

}
- - - ) +export async function createSession(userId: string) { + const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days + const session = await encrypt({ userId, expiresAt }) + + cookies().set('session', session, { + httpOnly: true, + secure: true, + expires: expiresAt, + sameSite: 'lax', + path: '/', + }) } +``` -function LoginButton() { - const { pending } = useFormStatus() +```js filename="app/actions/session.js" switcher +import 'server-only' +import { cookies } from 'next/headers' - const handleClick = (event) => { - if (pending) { - event.preventDefault() - } - } +export async function createSession(userId: string) { + const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days + const session = await encrypt({ userId, expiresAt }) - return ( - - ) + cookies().set('session', session, { + httpOnly: true, + secure: true, + expires: expiresAt, + sameSite: 'lax', + path: '/', + }) } ``` -```jsx filename="app/login/page.jsx" switcher -'use client' +Back in your Server Action, you can invoke the `createSession()` function after successfully creating the user or verifying their credentials, and use the [`redirect()`](/docs/app/building-your-application/routing/redirecting) API to redirect the user to the appropriate page: -import { authenticate } from '@/app/lib/actions' -import { useFormState, useFormStatus } from 'react-dom' +```ts filename="app/actions/auth.ts" switcher +import { createSession } from '@/app/lib/session' -export default function Page() { - const [errorMessage, dispatch] = useFormState(authenticate, undefined) +export async function signup(state: FormState, formData: FormData) { + // Previous steps: + // 1. Validate form fields + // 2. Prepare data for insertion into database + // 3. Insert the user into the database or call an Library API - return ( -
- - -
{errorMessage &&

{errorMessage}

}
- - - ) + // Current steps: + // 4. Create user session + await createSession(user.id) + // 5. Redirect user + redirect('/profile') } +``` -function LoginButton() { - const { pending } = useFormStatus() +```js filename="app/actions/auth.js" switcher +import { createSession } from '@/app/lib/session' - const handleClick = (event) => { - if (pending) { - event.preventDefault() - } - } +export async function signup(state, formData) { + // Previous steps: + // 1. Validate form fields + // 2. Prepare data for insertion into database + // 3. Insert the user into the database or call an Library API - return ( - - ) + // Current steps: + // 4. Create user session + await createSession(user.id) + // 5. Redirect user + redirect('/profile') } ``` -
+> **Tips**: +> +> - **Cookies should be set on the server** to prevent client-side tampering. +> - 🎥 Watch: Learn more about stateless sessions and authentication with Next.js → [YouTube (11 minutes)](https://www.youtube.com/watch?v=DJvM2lSPn6w). -For a more streamlined authentication setup in Next.js projects, especially when offering multiple login methods, consider using a comprehensive [authentication solution](#examples). +#### Updating (or refreshing) sessions -## Authorization +You can also extend the session's expiration time. This is useful for keeping the user logged in after they access the application again. For example: -Once a user is authenticated, you'll need to ensure the user is allowed to visit certain routes, and perform operations such as mutating data with Server Actions and calling Route Handlers. +```ts filename="app/actions/session.ts" switcher +import 'server-only' +import { cookies } from 'next/headers' -### Protecting Routes with Middleware +export async function updateSession() { + const session = cookies().get('session')?.value + const payload = await decrypt(session) -[Middleware](/docs/app/building-your-application/routing/middleware) in Next.js helps you control who can access different parts of your website. This is important for keeping areas like the user dashboard protected while having other pages like marketing pages be public. It's recommended to apply Middleware across all routes and specify exclusions for public access. + if (!session || !payload) { + return null + } -Here's how to implement Middleware for authentication in Next.js: + const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) + cookies().set('session', session, { + httpOnly: true, + secure: true, + expires: expires, + sameSite: 'lax', + path: '/', + }) +} +``` -#### Setting Up Middleware: +```js filename="app/actions/session.js" switcher +import 'server-only' +import { cookies } from 'next/headers' -- Create a `middleware.ts` or `.js` file in your project's root directory. -- Include logic to authorize user access, such as checking for authentication tokens. +eexport async function updateSession() { + const session = cookies().get('session').value + const payload = await decrypt(session) -#### Defining Protected Routes: + if (!session || !payload) { + return null + } -- Not all routes require authorization. Use the `matcher` option in your Middleware to specify any routes that do not require authorization checks. + const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) + cookies().set('session', session, { + httpOnly: true, + secure: true, + expires: expires, + sameSite: 'lax', + path: '/', + }) +} +``` -#### Middleware Logic: +> **Tips:** +> +> - If using an authentication library, check if they support refresh tokens. -- Write logic to verify if a user is authenticated. Check user roles or permissions for route authorization. +#### Deleting the session -#### Handling Unauthorized Access: +To delete the session, you can delete the cookie: -- Redirect unauthorized users to a login or error page as appropriate. +```ts filename="app/lib/session.ts" switcher +import 'server-only' +import { cookies } from 'next/headers' -Example Middleware file: +export function deleteSession() { + cookies().delete('session') +} +``` -```ts filename="middleware.ts" switcher -import type { NextRequest } from 'next/server' +```js filename="app/lib/session.js" switcher +import 'server-only' +import { cookies } from 'next/headers' -export function middleware(request: NextRequest) { - const currentUser = request.cookies.get('currentUser')?.value +export function deleteSession() { + cookies().delete('session') +} +``` - if (currentUser && !request.nextUrl.pathname.startsWith('/dashboard')) { - return Response.redirect(new URL('/dashboard', request.url)) - } +Then you can reuse the `deleteSession()` function in your application, for example, on logout: - if (!currentUser && !request.nextUrl.pathname.startsWith('/login')) { - return Response.redirect(new URL('/login', request.url)) - } -} +```ts filename="app/actions/auth.ts" switcher +import { cookies } from 'next/headers' +import { deleteSession } from '@/app/actions/session' -export const config = { - matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], +export async function logout() { + deleteSession() + redirect('/login') } ``` -```js filename="middleware.js" switcher -export function middleware(request) { - const currentUser = request.cookies.get('currentUser')?.value +```js filename="app/actions/auth.js" switcher +import { cookies } from 'next/headers' +import { deleteSession } from '@/app/actions/session' - if (currentUser && !request.nextUrl.pathname.startsWith('/dashboard')) { - return Response.redirect(new URL('/dashboard', request.url)) - } +export async function logout() { + deleteSession() + redirect('/login') +} +``` - if (!currentUser && !request.nextUrl.pathname.startsWith('/login')) { - return Response.redirect(new URL('/login', request.url)) - } +
+ + + +#### Setting and deleting cookies + +You can use [API Routes](/docs/pages/building-your-application/routing/api-routes) to set the session as a cookie on the server: + +```ts filename="pages/api/login.ts" switcher +import { serialize } from 'cookie' +import type { NextApiRequest, NextApiResponse } from 'next' + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + const sessionData = req.body + const encryptedSessionData = encrypt(sessionData) + + const cookie = serialize('session', encryptedSessionData, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + maxAge: 60 * 60 * 24 * 7, // One week + path: '/', + }) + res.setHeader('Set-Cookie', cookie) + res.status(200).json({ message: 'Successfully set cookie!' }) } +``` -export const config = { - matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], +```js filename="pages/api/login.js" switcher +import { serialize } from 'cookie' + +export default function handler(req, res) { + const sessionData = req.body + const encryptedSessionData = encrypt(sessionData) + + const cookie = serialize('session', encryptedSessionData, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + maxAge: 60 * 60 * 24 * 7, // One week + path: '/', + }) + res.setHeader('Set-Cookie', cookie) + res.status(200).json({ message: 'Successfully set cookie!' }) } ``` -This example uses [`Response.redirect`](https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static) for handling redirects early in the request pipeline, making it efficient and centralizing access control. + + +### Database Sessions + +To create and manage database sessions, you'll need to follow these steps: + +1. Create a table in your database to store session data (or check if your Auth Library handles this). +2. Implement functionality to insert, update, and delete sessions. +3. Encrypt the session ID before storing it in the user's browser, and ensure the database and cookie stay in sync (this is optional, but recommended for optimistic auth checks in [Middleware](#optimistic-checks-with-middleware-optional)). -For specific redirection needs, the `redirect` function can be used in Server Components, Route Handlers, and Server Actions to provide more control. This is useful for role-based navigation or context-sensitive scenarios. +Here's an example of how you can create a new database session, and encrypt the session ID before storing it in a cookie: -```ts filename="app/page.tsx" switcher -import { redirect } from 'next/navigation' +```ts filename="app/actions/session.ts" switcher +import cookies from 'next/headers' +import { db } from '@/app/lib/db' -export default function Page() { - // Logic to determine if a redirect is needed - const accessDenied = true - if (accessDenied) { - redirect('/login') - } +export async function createSession(id: number) { + const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) - // Define other routes and logic + // 1. Create a session in the database + const data = await db + .insert(sessions) + .values({ + userId: id, + expiresAt, + }) + // Return the session ID + .returning({ id: sessions.id }) + + const sessionId = data[0].id + + // 2. Encrypt the session ID + const session = await encrypt({ sessionId, expiresAt }) + + // 3. Store the session in cookies for optimistic auth checks + cookies().set('session', session, { + httpOnly: true, + secure: true, + expires: expiresAt, + sameSite: 'lax', + path: '/', + }) } ``` -```js filename="app/page.jsx" switcher -import { redirect } from 'next/navigation' +```js filename="app/actions/session.js" switcher +import cookies from 'next/headers' +import { db } from '@/app/lib/db' -export default function Page() { - // Logic to determine if a redirect is needed - const accessDenied = true - if (accessDenied) { - redirect('/login') - } +export async function createSession(id) { + const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) + + // 1. Create a session in the database + const data = await db + .insert(sessions) + .values({ + userId: id, + expiresAt, + }) + // Return the session ID + .returning({ id: sessions.id }) - // Define other routes and logic + const sessionId = data[0].id + + // 2. Encrypt the session ID + const session = await encrypt({ sessionId, expiresAt }) + + // 3. Store the session in cookies for optimistic auth checks + cookies().set('session', session, { + httpOnly: true, + secure: true, + expires: expiresAt, + sameSite: 'lax', + path: '/', + }) } ``` - +> **Tips**: +> +> - For faster data retrieval, consider using a database like [Vercel Redis](https://vercel.com/docs/storage/vercel-kv). However, you can also keep the session data in your primary database, and combine data requests to reduce the number of queries. +> - You may opt to use database sessions for more advanced use cases, such as keeping track of the last time a user logged in, or number of active devices. You can also give your users the ability to log out of all devices. -After successful authentication, it's important to manage user navigation based on their roles. For example, an admin user might be redirected to an admin dashboard, while a regular user is sent to a different page. This is important for role-specific experiences and conditional navigation, such as prompting users to complete their profile if needed. +After implementing session management, you'll need to add authorization logic to control what users can access and do within your application. Continue to the [Authorization](#authorization) section to learn more. -When setting up authorization, it's important to ensure that the main security checks happen where your app accesses or changes data. While Middleware can be useful for initial validation, it should not be the sole line of defense in protecting your data. The bulk of security checks should be performed in the Data Access Layer (DAL). + -### Protecting API Routes - -API Routes in Next.js are essential for handling server-side logic and data management. It's crucial to secure these routes to ensure that only authorized users can access specific functionalities. This typically involves verifying the user's authentication status and their role-based permissions. - -Here's an example of securing an API Route: +**Creating a Session on the Server**: -```ts filename="pages/api/route.ts" switcher +```ts filename="pages/api/create-session.ts" switcher +import db from '../../lib/db' import { NextApiRequest, NextApiResponse } from 'next' export default async function handler( req: NextApiRequest, res: NextApiResponse ) { - const session = await getSession(req) - - // Check if the user is authenticated - if (!session) { - res.status(401).json({ - error: 'User is not authenticated', + try { + const user = req.body + const sessionId = generateSessionId() + await db.insertSession({ + sessionId, + userId: user.id, + createdAt: new Date(), }) - return - } - // Check if the user has the 'admin' role - if (session.user.role !== 'admin') { - res.status(401).json({ - error: 'Unauthorized access: User does not have admin privileges.', - }) - return + res.status(200).json({ sessionId }) + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }) } - - // Proceed with the route for authorized users - // ... implementation of the API Route } ``` -```js filename="pages/api/route.js" switcher -export default async function handler(req, res) { - const session = await getSession(req) +```js filename="pages/api/create-session.js" switcher +import db from '../../lib/db' - // Check if the user is authenticated - if (!session) { - res.status(401).json({ - error: 'User is not authenticated', +export default async function handler(req, res) { + try { + const user = req.body + const sessionId = generateSessionId() + await db.insertSession({ + sessionId, + userId: user.id, + createdAt: new Date(), }) - return - } - // Check if the user has the 'admin' role - if (session.user.role !== 'admin') { - res.status(401).json({ - error: 'Unauthorized access: User does not have admin privileges.', - }) - return + res.status(200).json({ sessionId }) + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }) } - - // Proceed with the route for authorized users - // ... implementation of the API Route } ``` -This example demonstrates an API Route with a two-tier security check for authentication and authorization. It first checks for an active session, and then verifies if the logged-in user is an 'admin'. This approach ensures secure access, limited to authenticated and authorized users, maintaining robust security for request processing. - - +## Authorization -This approach, highlighted in [this security blog](/blog/security-nextjs-server-components-actions), advocates for consolidating all data access within a dedicated DAL. This strategy ensures consistent data access, minimizes authorization bugs, and simplifies maintenance. To ensure comprehensive security, consider the following key areas: +Once a user is authenticated and a session is created, you can implement authorization to control what the user can access and do within your application. -- Server Actions: Implement security checks in server-side processes, especially for sensitive operations. -- Route Handlers: Manage incoming requests with security measures to ensure access is limited to authorized users. -- Data Access Layer (DAL): Directly interacts with the database and is crucial for validating and authorizing data transactions. It's vital to perform critical checks within the DAL to secure data at its most crucial interaction point—access or modification. +There are two main types of authorization checks: -For a detailed guide on securing the DAL, including example code snippets and advanced security practices, refer to our [Data Access Layer section](/blog/security-nextjs-server-components-actions#data-access-layer) of the security guide. +1. **Optimistic**: Checks if the user is authorized to access a route or perform an action using the session data stored in the cookie. These checks are useful for quick operations, such as showing/hiding UI elements or redirecting users based on permissions or roles. +2. **Secure**: Checks if the user is authorized to access a route or perform an action using the session data stored in the database. These checks are more secure and are used for operations that require access to sensitive data or actions. -### Protecting Server Actions +For both cases, we recommend: -It is important to treat [Server Actions](/docs/app/building-your-application/data-fetching/server-actions-and-mutations) with the same security considerations as public-facing API endpoints. Verifying user authorization for each action is crucial. Implement checks within Server Actions to determine user permissions, such as restricting certain actions to admin users. +- Creating a [Data Access Layer](#creating-a-data-access-layer-dal) to centralize your authorization logic +- Using [Data Transfer Objects (DTO)](#using-data-transfer-objects-dto) to only return the necessary data +- Optionally use [Middleware](#optimistic-checks-with-middleware-optional) to perform optimistic checks. -In the example below, we check the user's role before allowing the action to proceed: +### Optimistic checks with Middleware (Optional) -```ts filename="app/lib/actions.ts" switcher -'use server' +There are some cases where you may want to use [Middleware](/docs/app/building-your-application/routing/middleware): -// ... +- To perform optimistic checks and redirect users based on their permissions. Since Middleware runs on every route, it's a good way to centralize your redirect logic and pre-filter unauthorized users. +- To protect static routes that share data between users (e.g. content behind a paywall). -export async function serverAction() { - const session = await getSession() - const userRole = session?.user?.role +For example: - // Check if user is authorized to perform the action - if (userRole !== 'admin') { - throw new Error('Unauthorized access: User does not have admin privileges.') +```tsx filename="middleware.ts" switcher +import { NextRequest, NextResponse } from 'next/server' +import { decrypt } from '@/app/lib/session' +import { cookies } from 'next/headers' + +// 1. Specify protected and public routes +const protectedRoutes = ['/dashboard'] +const publicRoutes = ['/login', '/signup', '/'] + +export default async function middleware(req: NextRequest) { + // 2. Check if the current route is protected or public + const path = req.nextUrl.pathname + const isProtectedRoute = protectedRoutes.includes(path) + const isPublicRoute = publicRoutes.includes(path) + + // 3. Decrypt the session from the cookie + const cookie = cookies().get('session')?.value + const session = await decrypt(cookie) + + // 5. Redirect to /login if the user is not authenticated + if (isProtectedRoute && !session?.userId) { + return NextResponse.redirect(new URL('/login', req.nextUrl)) } - // Proceed with the action for authorized users - // ... implementation of the action + // 6. Redirect to /dashboard if the user is authenticated + if ( + isPublicRoute && + session?.userId && + !req.nextUrl.pathname.startsWith('/dashboard') + ) { + return NextResponse.redirect(new URL('/dashboard', req.nextUrl)) + } + + return NextResponse.next() +} + +// Routes Middleware should not run on +export const config = { + matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], } ``` -```js filename="app/lib/actions.js" switcher -'use server' +```js filename="middleware.js" switcher +import { NextResponse } from 'next/server' +import { decrypt } from '@/app/lib/session' +import { cookies } from 'next/headers' -// ... +// 1. Specify protected and public routes +const protectedRoutes = ['/dashboard'] +const publicRoutes = ['/login', '/signup', '/'] -export async function serverAction() { - const session = await getSession() - const userRole = session?.user?.role +export default async function middleware(req) { + // 2. Check if the current route is protected or public + const path = req.nextUrl.pathname + const isProtectedRoute = protectedRoutes.includes(path) + const isPublicRoute = publicRoutes.includes(path) - // Check if user is authorized to perform the action - if (userRole !== 'admin') { - throw new Error('Unauthorized access: User does not have admin privileges.') + // 3. Decrypt the session from the cookie + const cookie = cookies().get('session')?.value + const session = await decrypt(cookie) + + // 5. Redirect to /login if the user is not authenticated + if (isProtectedRoute && !session?.userId) { + return NextResponse.redirect(new URL('/login', req.nextUrl)) } - // Proceed with the action for authorized users - // ... implementation of the action + // 6. Redirect to /dashboard if the user is authenticated + if ( + isPublicRoute && + session?.userId && + !req.nextUrl.pathname.startsWith('/dashboard') + ) { + return NextResponse.redirect(new URL('/dashboard', req.nextUrl)) + } + + return NextResponse.next() +} + +// Routes Middleware should not run on +export const config = { + matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], } ``` -### Protecting Route Handlers +However, since Middleware runs on every route, including [prefetched](/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching) routes, it's important use Middleware only to read the session from the cookie (optimistic checks), and avoid database checks or heavy computations to prevent performance issues. -Route Handlers in Next.js play a vital role in managing incoming requests. Just like Server Actions, they should be secured to ensure that only authorized users can access certain functionalities. This often involves verifying the user's authentication status and their permissions. +While Middleware can be useful for initial checks, it should not be your only line of defense in protecting your data. The majority of security checks should be performed as close as possible to your data source, see [Data Access Layer](#creating-a-data-access-layer-dal) for more information. -Here's an example of securing a Route Handler: +> **Tips**: +> +> - In Middleware, you can also read cookies using `req.cookies.get('session).value`. +> - Middleware uses the [Edge Runtime](/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes), check if your Auth library and session management library are compatible. +> - You can use the `matcher` property in the Middleware to specify which routes Middleware should run on. Although, for auth, it's recommended Middleware runs on all routes. -```ts filename="app/api/route.ts" switcher -export async function GET() { - // User authentication and role verification - const session = await getSession() + - // Check if the user is authenticated - if (!session) { - return new Response(null, { status: 401 }) // User is not authenticated +### Creating a Data Access Layer (DAL) + +We recommend creating a Data Access Layer (DAL) to centralize your data requests and authorization logic. The DAL should include a function that verifies the user's session as they interact with your application. + +At the very least, the function should check if the session is valid, then redirect or return the user information needed to make further requests. + +For example, create a separate file for your DAL that includes a `verifySession()` function. Then use React's [cache](https://react.dev/reference/react/cache) to memoize the return value of the function during a React render pass: + +```tsx filename="app/lib/dal.ts" switcher +import 'server-only' + +import { cookies } from 'next/headers' +import { decrypt } from '@/app/lib/session' + +export const verifySession = cache(async () => { + const cookie = cookies().get('session')?.value + const session = await decrypt(cookie) + + if (!session?.userId) { + redirect('/login') } - // Check if the user has the 'admin' role - if (session.user.role !== 'admin') { - return new Response(null, { status: 403 }) // User is authenticated but does not have the right permissions + return { isAuth: true, userId: session.userId } +}) +``` + +```js filename="app/lib/dal.js" switcher +import 'server-only' + +import { cookies } from 'next/headers' +import { decrypt } from '@/app/lib/session' + +export const verifySession = cache(async () => { + const cookie = cookies().get('session').value + const session = await decrypt(cookie) + + if (!session.userId) { + redirect('/login') } - // Data fetching for authorized users -} + return { isAuth: true, userId: session.userId } +}) ``` -```js filename="app/api/route.js" switcher -export async function GET() { - // User authentication and role verification - const session = await getSession() +You can then invoke the `verifySession()` function in your data requests and Server Actions before performing any operations: - // Check if the user is authenticated - if (!session) { - return new Response(null, { status: 401 }) // User is not authenticated +```tsx filename="app/lib/dal.ts" switcher +// ... + +export const getUser = cache(async () => { + const session = await verifySession() + if (!session) return null + + try { + const data = await db.query.users.findMany({ + where: eq(users.id, session.userId), + // Explicitly return the columns you need rather than the whole user object + columns: { + id: true, + name: true, + email: true, + }, + }) + + const user = data[0] + + return user + } catch (error) { + console.log('Failed to fetch user') + return null } +}) +``` - // Check if the user has the 'admin' role - if (session.user.role !== 'admin') { - return new Response(null, { status: 403 }) // User is authenticated but does not have the right permissions +```jsx filename="app/lib/dal.js" switcher +// ... + +export const getUser = cache(async () => { + const session = await verifySession() + if (!session) return null + + try { + const data = await db.query.users.findMany({ + where: eq(users.id, session.userId), + // Explicitly return the columns you need rather than the whole user object + columns: { + id: true, + name: true, + email: true, + }, + }) + + const user = data[0] + + return user + } catch (error) { + console.log('Failed to fetch user') + return null } +}) +``` + +> **Tip**: +> +> - A DAL can be used for runtime personalized data. However, for static routes that share data between users, data will be fetched at build time and not request time. Use [Middleware](#optimistic-checks-with-middleware-optional) to protect static routes. +> - For secure checks, you can check if the session is valid by comparing the session ID with your database. Use React's [cache](https://react.dev/reference/react/cache) function to avoid unnecessary duplicate requests to the database during a render pass. +> - You may wish to consolidate related data requests in a JavaScript class that runs `verifySession()` before any methods. + +### Using Data Transfer Objects (DTO) + +When retrieving data, it's recommended you return only the necessary data that will be used in your application, and not entire objects. For example, if you're fetching user data, you might only return the user's ID, name, and username, rather than the entire user object which could contain passwords, phone numbers, etc. + +However, if you have no control over the data structure, or are working in a team where you want to avoid whole objects being passed to the client, you can use strategies such as specifying what fields are ok to be exposed to the client. + +```tsx filename="app/lib/dto.ts" switcher +import 'server-only' +import { getUser } from '@/app/lib/dal' + +function canSeeUsername(viewer: User) { + return true +} + +function canSeePhoneNumber(viewer: User, team: string) { + return viewer.isAdmin || team === viewer.team +} + +export async function getProfileDTO(slug: string) { + const data = await db.query.users.findMany({ + where: eq(users.slug, slug), + // Return specific columns here + }) + const user = data[0] + + const currentUser = await getUser(user.id) - // Data fetching for authorized users + // Or return only what's specific to the query here + return { + username: canSeeUsername(currentUser) ? user.username : null, + phonenumber: canSeePhoneNumber(currentUser, user.team) + ? user.phonenumber + : null, + } } ``` -This example demonstrates a Route Handler with a two-tier security check for authentication and authorization. It first checks for an active session, and then verifies if the logged-in user is an 'admin'. This approach ensures secure access, limited to authenticated and authorized users, maintaining robust security for request processing. +```js filename="app/lib/dto.js" switcher +import 'server-only' +import { getUser } from '@/app/lib/dal' -### Authorization Using Server Components +function canSeeUsername(viewer) { + return true +} -[Server Components](/docs/app/building-your-application/rendering/server-components) in Next.js are designed for server-side execution and offer a secure environment for integrating complex logic like authorization. They enable direct access to back-end resources, optimizing performance for data-heavy tasks and enhancing security for sensitive operations. +function canSeePhoneNumber(viewer, team) { + return viewer.isAdmin || team === viewer.team +} + +export async function getProfileDTO(slug) { + const data = await db.query.users.findMany({ + where: eq(users.slug, slug), + // Return specific columns here + }) + const user = data[0] + + const currentUser = await getUser(user.id) -In Server Components, a common practice is to conditionally render UI elements based on the user's role. This approach enhances user experience and security by ensuring users only access content they are authorized to view. + // Or return only what's specific to the query here + return { + username: canSeeUsername(currentUser) ? user.username : null, + phonenumber: canSeePhoneNumber(currentUser, user.team) + ? user.phonenumber + : null, + } +} +``` -**Example:** +By centralizing your data requests and authorization logic in a DAL and using DTOs, you can ensure that all data requests are secure and consistent, making it it easier to maintain, audit, and debug as your application scales. + +> **Good to know**: +> +> - There are a couple of different ways you can define a DTO, from using `toJSON()`, to individual functions like the example above, to JS classes. Since these are JavaScript patterns and not a React or Next.js feature, we recommend doing some research to find the best pattern for your application. +> - Learn more about security best practices in our [Security in Next.js article](/blog/security-nextjs-server-components-actions). + +### Server Components + +You can do auth checks and use the [`redirect()`](/docs/app/api-reference/functions/redirect) API in [Server Components](/docs/app/building-your-application/rendering/server-components). This is useful for role-based access. For example, to conditionally render components based on the user's role: ```tsx filename="app/dashboard/page.tsx" switcher -export default async function Dashboard() { - const session = await getSession() +import { verifySession } from '@/app/lib/dal' + +export default function Dashboard() { + const session = await verifySession() const userRole = session?.user?.role // Assuming 'role' is part of the session object if (userRole === 'admin') { - return // Component for admin users + return } else if (userRole === 'user') { - return // Component for regular users + return } else { - return // Component shown for unauthorized access + redirect('/login') } } ``` ```jsx filename="app/dashboard/page.jsx" switcher +import { verifySession } from '@/app/lib/dal' + export default function Dashboard() { - const session = await getSession() - const userRole = session?.user?.role // Assuming 'role' is part of the session object + const session = await verifySession() + const userRole = session.role // Assuming 'role' is part of the session object if (userRole === 'admin') { - return // Component for admin users + return } else if (userRole === 'user') { - return // Component for regular users + return } else { - return // Component shown for unauthorized access + redirect('/login') } } ``` -In this example, the Dashboard component renders different UIs for 'admin', 'user', and unauthorized roles. This pattern ensures that each user interacts only with components appropriate to their role, enhancing both security and user experience. +In the example, we use the `verifySession()` function from our DAL to check for 'admin', 'user', and unauthorized roles. This pattern ensures that each user interacts only with components appropriate to their role. - +#### Layouts and auth checks -### Best Practices +Due to [Partial Rendering](/docs/app/building-your-application/routing/linking-and-navigating#4-partial-rendering), be cautious when doing checks in [Layouts](/docs/app/building-your-application/routing/pages-and-layouts) as these don't re-render on navigation. Instead, you should do the checks close to your data source or the component that'll be conditionally rendered. -- **Secure Session Management**: Prioritize the security of session data to prevent unauthorized access and data breaches. Use encryption and secure storage practices. -- **Dynamic Role Management**: Use a flexible system for user roles to easily adjust to changes in permissions and roles, avoiding hardcoded roles. -- **Security-First Approach**: In all aspects of authorization logic, prioritize security to safeguard user data and maintain the integrity of your application. This includes thorough testing and considering potential security vulnerabilities. +For example, consider a shared layout that fetches the user data and displays the user image in a nav. Instead of doing the auth check in the layout, you should fetch the user data (`getUser()`) in the layout and do the auth check in your DAL. -## Session Management +```tsx filename="app/layout.tsx" switcher +export default async function Layout({ + children, +}: { + children: React.ReactNode; +}) { + const user = await getUser(); + + return ( + // ... + ) +} +``` -Session management involves tracking and managing a user's interaction with the application over time, ensuring that their authenticated state is preserved across different parts of the application. +```jsx filename="app/layout.js" switcher +export default async function Layout({ children }) { + const user = await getUser(); -This prevents the need for repeated logins, enhancing both security and user convenience. There are two primary methods used for session management: cookie-based and database sessions. + return ( + // ... + ) +} +``` -### Cookie-Based Sessions +This guarantees that wherever `getUser()` is called within your application, the auth check is performed. -> **🎥 Watch:** Learn more about cookie-based sessions and authentication with Next.js → [YouTube (11 minutes)](https://www.youtube.com/watch?v=DJvM2lSPn6w). +```ts filename="app/lib/dal.ts" switcher +export const getUser = cache(async () => { + const session = await verifySession() + if (!session) return null -Cookie-based sessions manage user data by storing encrypted session information directly in browser cookies. Upon user login, this encrypted data is stored in the cookie. Each subsequent server request includes this cookie, minimizing the need for repeated server queries and enhancing client-side efficiency. + // Get user ID from session and fetch data +}) +``` -However, this method requires careful encryption to protect sensitive data, as cookies are susceptible to client-side security risks. Encrypting session data in cookies is key to safeguarding user information from unauthorized access. It ensures that even if a cookie is stolen, the data inside remains unreadable. +```js filename="app/lib/dal.js" switcher +export const getUser = cache(async () => { + const session = await verifySession() + if (!session) return null -Additionally, while individual cookies are limited in size (typically around 4KB), techniques like cookie-chunking can overcome this limitation by dividing large session data into multiple cookies. + // Get user ID from session and fetch data +}) +``` -Setting a cookie in a Next.js project might look something like this: +> **Good to know:** +> +> - A common pattern in SPAs is to `return null` in a layout or a top-level component if a user is not authorized. Since Next.js applications have multiple entry points, this pattern is **not recommended** since will not prevent nested layouts and pages and Server Actions from being accessed. -**Setting a cookie on the server:** +#### Server Actions - +Treat [Server Actions](/docs/app/building-your-application/data-fetching/server-actions-and-mutations) with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation. -```ts filename="pages/api/login.ts" switcher -import { serialize } from 'cookie' -import type { NextApiRequest, NextApiResponse } from 'next' +In the example below, we check the user's role before allowing the action to proceed: -export default function handler(req: NextApiRequest, res: NextApiResponse) { - const sessionData = req.body - const encryptedSessionData = encrypt(sessionData) +```ts filename="app/lib/actions.ts" switcher +'use server' +import { verifySession } from '@/app/lib/dal' - const cookie = serialize('session', encryptedSessionData, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - maxAge: 60 * 60 * 24 * 7, // One week - path: '/', - }) - res.setHeader('Set-Cookie', cookie) - res.status(200).json({ message: 'Successfully set cookie!' }) +export async function serverAction(formData: FormData) { + const session = await verifySession() + const userRole = session?.user?.role + + // Return early if user is not authorized to perform the action + if (userRole !== 'admin') { + return null + } + + // Proceed with the action for authorized users } ``` -```js filename="pages/api/login.js" switcher -import { serialize } from 'cookie' +```js filename="app/lib/actions.js" switcher +'use server' +import { verifySession } from '@/app/lib/dal' -export default function handler(req, res) { - const sessionData = req.body - const encryptedSessionData = encrypt(sessionData) +export async function serverAction() { + const session = await verifySession() + const userRole = session.user.role - const cookie = serialize('session', encryptedSessionData, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - maxAge: 60 * 60 * 24 * 7, // One week - path: '/', - }) - res.setHeader('Set-Cookie', cookie) - res.status(200).json({ message: 'Successfully set cookie!' }) + // Return early if user is not authorized to perform the action + if (userRole !== 'admin') { + return null + } + + // Proceed with the action for authorized users } ``` - +#### Route Handlers - +Treat [Route Handlers](/docs/app/building-your-application/routing/route-handlers) with the same security considerations as public-facing API endpoints, and verify if the user is allowed to access the the Route Handler. -```ts filename="app/actions.ts" switcher -'use server' +Here's an example of securing a Route Handler: -import { cookies } from 'next/headers' +```ts filename="app/api/route.ts" switcher +import { verifySession } from '@/app/lib/dal' -export async function handleLogin(sessionData) { - const encryptedSessionData = encrypt(sessionData) // Encrypt your session data - cookies().set('session', encryptedSessionData, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - maxAge: 60 * 60 * 24 * 7, // One week - path: '/', - }) - // Redirect or handle the response after setting the cookie +export async function GET() { + // User authentication and role verification + const session = await verifySession() + + // Check if the user is authenticated + if (!session) { + // User is not authenticated + return new Response(null, { status: 401 }) + } + + // Check if the user has the 'admin' role + if (session.user.role !== 'admin') { + // User is authenticated but does not have the right permissions + return new Response(null, { status: 403 }) + } + + // Continue for authorized users } ``` -```js filename="app/actions.js" switcher -'use server' +```js filename="app/api/route.js" switcher +import { verifySession } from '@/app/lib/dal' -import { cookies } from 'next/headers' +export async function GET() { + // User authentication and role verification + const session = await verifySession() -export async function handleLogin(sessionData) { - const encryptedSessionData = encrypt(sessionData) // Encrypt your session data - cookies().set('session', encryptedSessionData, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - maxAge: 60 * 60 * 24 * 7, // One week - path: '/', - }) - // Redirect or handle the response after setting the cookie + // Check if the user is authenticated + if (!session) { + // User is not authenticated + return new Response(null, { status: 401 }) + } + + // Check if the user has the 'admin' role + if (session.user.role !== 'admin') { + // User is authenticated but does not have the right permissions + return new Response(null, { status: 403 }) + } + + // Continue for authorized users } ``` -**Accessing the session data stored in the cookie in a server component:** +The example above demonstrates a Route Handler with a two-tier security check. It first checks for an active session, and then verifies if the logged-in user is an 'admin'. -```tsx filename="app/page.tsx" switcher -import { cookies } from 'next/headers' +## Context Providers + +Using context providers for auth work due to [interleaving](/docs/app/building-your-application/rendering/composition-patterns#interleaving-server-and-client-components). However, React `context` is not supported in Server Components, making them only applicable to Client Components. + +This works, however, any children Server Components will be rendered on the server first, and will not have access to the context provider’s session data: -export async function getSessionData(req) { - const encryptedSessionData = cookies().get('session')?.value - return encryptedSessionData ? JSON.parse(decrypt(encryptedSessionData)) : null +```tsx filename="app/layout.ts" switcher +import { ContextProvider } from 'auth-lib' + +export default function RootLayout({ children }) { + return ( + + + {children} + + + ) } ``` -```jsx filename="app/page.jsx" switcher -import { cookies } from 'next/headers' +```tsx filename="app/layout.ts" switcher +import { ContextProvider } from 'auth-lib' -export async function getSessionData(req) { - const encryptedSessionData = cookies().get('session')?.value - return encryptedSessionData ? JSON.parse(decrypt(encryptedSessionData)) : null +export default function RootLayout({ children }) { + return ( + + + {children} + + + ) } ``` - +```tsx filename="app/ui/profile.ts switcher +"use client"; -### Database Sessions +import { useSession } from "auth-lib"; -Database session management involves storing session data on the server, with the user's browser only receiving a session ID. This ID references the session data stored server-side, without containing the data itself. This method enhances security, as it keeps sensitive session data away from the client-side environment, reducing the risk of exposure to client-side attacks. Database sessions are also more scalable, accommodating larger data storage needs. +export default function Profile() { + const { userId } = useSession(); + const { data } = useSWR(`/api/user/${userId}`, fetcher) -However, this approach has its tradeoffs. It can increase performance overhead due to the need for database lookups at each user interaction. Strategies like session data caching can help mitigate this. Additionally, reliance on the database means that session management is as reliable as the database's performance and availability. + return ( + // ... + ); +} +``` -Here's a simplified example of implementing database sessions in a Next.js application: +```jsx filename="app/ui/profile.js switcher +"use client"; -**Creating a Session on the Server**: +import { useSession } from "auth-lib"; + +export default function Profile() { + const { userId } = useSession(); + const { data } = useSWR(`/api/user/${userId}`, fetcher) + + return ( + // ... + ); +} +``` + +If session data is needed in Client Components (e.g. for client-side data fetching),use React’s [`taintUniqueValue`](https://react.dev/reference/react/experimental_taintUniqueValue) API to prevent sensitive session data from being exposed to the client. + + -```ts filename="pages/api/create-session.ts" switcher -import db from '../../lib/db' +### Creating a Data Access Layer (DAL) + +#### Protecting API Routes + +API Routes in Next.js are essential for handling server-side logic and data management. It's crucial to secure these routes to ensure that only authorized users can access specific functionalities. This typically involves verifying the user's authentication status and their role-based permissions. + +Here's an example of securing an API Route: + +```ts filename="pages/api/route.ts" switcher import { NextApiRequest, NextApiResponse } from 'next' export default async function handler( req: NextApiRequest, res: NextApiResponse ) { - try { - const user = req.body - const sessionId = generateSessionId() - await db.insertSession({ - sessionId, - userId: user.id, - createdAt: new Date(), - }) + const session = await getSession(req) - res.status(200).json({ sessionId }) - } catch (error) { - res.status(500).json({ error: 'Internal Server Error' }) + // Check if the user is authenticated + if (!session) { + res.status(401).json({ + error: 'User is not authenticated', + }) + return } -} -``` -```js filename="pages/api/create-session.js" switcher -import db from '../../lib/db' - -export default async function handler(req, res) { - try { - const user = req.body - const sessionId = generateSessionId() - await db.insertSession({ - sessionId, - userId: user.id, - createdAt: new Date(), + // Check if the user has the 'admin' role + if (session.user.role !== 'admin') { + res.status(401).json({ + error: 'Unauthorized access: User does not have admin privileges.', }) - - res.status(200).json({ sessionId }) - } catch (error) { - res.status(500).json({ error: 'Internal Server Error' }) + return } -} -``` - - - - - -```js -import db from './lib/db' -export async function createSession(user) { - const sessionId = generateSessionId() // Generate a unique session ID - await db.insertSession({ sessionId, userId: user.id, createdAt: new Date() }) - return sessionId + // Proceed with the route for authorized users + // ... implementation of the API Route } ``` -**Retrieving a Session in Middleware or Server-Side Logic**: +```js filename="pages/api/route.js" switcher +export default async function handler(req, res) { + const session = await getSession(req) -```js -import { cookies } from 'next/headers' -import db from './lib/db' + // Check if the user is authenticated + if (!session) { + res.status(401).json({ + error: 'User is not authenticated', + }) + return + } + + // Check if the user has the 'admin' role + if (session.user.role !== 'admin') { + res.status(401).json({ + error: 'Unauthorized access: User does not have admin privileges.', + }) + return + } -export async function getSession() { - const sessionId = cookies().get('sessionId')?.value - return sessionId ? await db.findSession(sessionId) : null + // Proceed with the route for authorized users + // ... implementation of the API Route } ``` - - -### Selecting Session Management in Next.js - -Deciding between cookie-based and database sessions in Next.js depends on your application's needs. Cookie-based sessions are simpler and suit smaller applications with lower server load but may offer less security. Database sessions, while more complex, provide better security and scalability, ideal for larger, data-sensitive applications. - -With [authentication solutions](#examples) such as [NextAuth.js](https://authjs.dev/guides/upgrade-to-v5), session management becomes more efficient, using either cookies or database storage. This automation simplifies the development process, but it's important to understand the session management method used by your chosen solution. Ensure it aligns with your application's security and performance requirements. +This example demonstrates an API Route with a two-tier security check for authentication and authorization. It first checks for an active session, and then verifies if the logged-in user is an 'admin'. This approach ensures secure access, limited to authenticated and authorized users, maintaining robust security for request processing. -Regardless of your choice, prioritize security in your session management strategy. For cookie-based sessions, using secure and HTTP-only cookies is crucial to protect session data. For database sessions, regular backups and secure handling of session data are essential. Implementing session expiry and cleanup mechanisms is vital in both approaches to prevent unauthorized access and maintain application performance and reliability. + -## Examples +## Resources -Here are authentication solutions compatible with Next.js, please refer to the quickstart guides below to learn how to configure them in your Next.js application: +Now that you've learned about authentication in Next.js, here are Next.js-compatible libraries and resources to help you implement secure authentication and session management: -{/* TODO: Change link to authjs.dev when new documentation is ready */} +### Auth Libraries - [Auth0](https://auth0.com/docs/quickstart/webapp/nextjs/01-login) - [Clerk](https://clerk.com/docs/quickstarts/nextjs) - [Kinde](https://kinde.com/docs/developer-tools/nextjs-sdk) - [Lucia](https://lucia-auth.com/getting-started/nextjs-app) -- [NextAuth.js](https://authjs.dev/getting-started/migrating-to-v5) +- [NextAuth.js](https://authjs.dev/getting-started/installation?framework=next.js) - [Supabase](https://supabase.com/docs/guides/getting-started/quickstarts/nextjs) - [Stytch](https://stytch.com/docs/guides/quickstarts/nextjs) + +## Session Management Libraries + - [Iron Session](https://github.com/vvo/iron-session) +- [Jose](https://github.com/panva/jose) ## Further Reading To continue learning about authentication and security, check out the following resources: +- [How to think about security in Next.js](/blog/security-nextjs-server-components-actions) - [Understanding XSS Attacks](https://vercel.com/guides/understanding-xss-attacks) - [Understanding CSRF Attacks](https://vercel.com/guides/understanding-csrf-attacks) +- [The Copenhagen Book](https://thecopenhagenbook.com/)