From d12a5f36e2db63d12dae7ffcab2f3396802c484f Mon Sep 17 00:00:00 2001 From: Delba de Oliveira Date: Mon, 11 Mar 2024 13:57:34 +0000 Subject: [PATCH 01/22] Update intro --- .../09-authentication/index.mdx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) 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 4e7353e6164d5..5d8f1d3137cbe 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,15 +1,21 @@ --- 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 Next.js. --- -To implement authentication in Next.js, familiarize yourself with three foundational concepts: +This page will guide you through implementing authentication in Next.js, it covers best practices and patterns so you can choose the best solution 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 be familiar with the following 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. +- **[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, or use a 3rd-party service like Google. + - **[Auth Providers](#auth-providers)**: Services that handle the authentication process for you. They often provide a set of helpful APIs and/or SDKs, making it easier and safer to implement auth in your application. +- **[Session Management](#session-management)**: Tracks the user's auth state (e.g. logged-in) across requests. + - **[Session Management Libraries](#session-management-libraries)**: Libraries that help you manage user sessions, including setting, storing and retrieving cookie data, and handling expiration. +- **[Authorization](#authorization)**: Decides what parts of the application the user is allowed to access. It includes redirecting users to different routes based on their auth state or role. + +This diagram illustrates the flow, and what Next.js features you can use for authentication, session management, and authorization: + +{/* TODO: Auth Diagram */} ## Authentication From b02ba4af48f8b049939f7450069aba820a8589f3 Mon Sep 17 00:00:00 2001 From: Delba de Oliveira Date: Mon, 11 Mar 2024 13:59:28 +0000 Subject: [PATCH 02/22] Structure (wip) --- .../09-authentication/index.mdx | 713 +++++++++--------- 1 file changed, 356 insertions(+), 357 deletions(-) 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 5d8f1d3137cbe..9e7bbe102be03 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 @@ -19,22 +19,168 @@ This diagram illustrates the flow, and what Next.js features you can use for aut ## 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 [Server Actions](/docs/app/building-your-application/rendering/server-components) with React [`
`](https://react.dev/reference/react-dom/components/form) to create login or sign-up UI. Since Server Actions execute on the server, they provide a secure environment for handling sensitive operations. + +1. The user submits their credentials through a login or registration form. +2. The form invokes a Server Action, which calls your authentication provider's API to verify the user's credentials, and manage the user's session. +3. If verification is successful, the user is authenticated and redirected to a protected route. +4. If verification is unsuccessful, an error message is shown. + +For example, consider a login form where users can enter their credentials: + +```tsx filename="app/login/page.tsx" switcher +import { login } from '@/app/lib/auth' + +export default function Page() { + return ( + + + + +
+ ) +} +``` + +```jsx filename="app/login/page.jsx" switcher +import { login } from '@/app/lib/auth' + +export default function Page() { + return ( +
+ + s +
+ ) +} +``` + +The form above has two input fields for capturing the user's email and password. On submission, it calls the `login` Server Action. + +Inside the Server Action, you can do the following: + +1. Extract and validate the form input values. For example, check if the email is valid and the password is not empty. +2. Call your authentication provider's API to verify the user's credentials. +3. If the credentials are valid, create a session for the user that persists across requests. See [Session Management](#session-management) for more details. + +```ts filename="app/lib/actions.ts" switcher +'use server' + +import { signIn } from 'auth-provider' + +export async function login(formData: FormData) { + try { + await signIn('credentials', formData) + redirect('/dashboard') + } catch (error) { + if (error) { + switch (error.type) { + case 'CredentialsSignin': + return 'Invalid credentials.' + default: + return 'Something went wrong.' + } + } + throw error + } +} +``` -### Authentication Strategies +```js filename="app/lib/actions.js" switcher +'use server' -Modern web applications commonly use several authentication strategies: +import { signIn } from 'auth-provider' + +export async function authenticate(formData) { + try { + await signIn('credentials', formData) + } catch (error) { + if (error) { + switch (error.type) { + case 'CredentialsSignin': + return 'Invalid credentials.' + default: + return 'Something went wrong.' + } + } + throw error + } +} +``` -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. +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: -Selecting an authentication strategy should align with your application's specific requirements, user interface considerations, and security objectives. +```tsx filename="app/login/page.tsx" switcher +'use client' -### Implementing Authentication +import { authenticate } from '@/app/lib/actions' +import { useFormState, useFormStatus } from 'react-dom' -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: +export default function Page() { + const [errorMessage, dispatch] = useFormState(authenticate, undefined) + + return ( +
+ + +
{errorMessage &&

{errorMessage}

}
+ + + ) +} + +function LoginButton() { + const { pending } = useFormStatus() + + return ( + + ) +} +``` + +```jsx filename="app/login/page.jsx" switcher +'use client' + +import { authenticate } from '@/app/lib/actions' +import { useFormState, useFormStatus } from 'react-dom' + +export default function Page() { + const [errorMessage, dispatch] = useFormState(authenticate, undefined) + + return ( +
+ + +
{errorMessage &&

{errorMessage}

}
+ + + ) +} + +function LoginButton() { + const { pending } = useFormStatus() + + return ( + + ) +} +``` + +### Logout functionality + +### Auth Providers + +The following Auth Providers offer a range of authentication solutions for Next.js applications: + +
@@ -167,166 +313,246 @@ export default async function handler(req, res) { - +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: -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. +- **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. -Consider a login form where users can input their credentials: +For a more streamlined authentication setup in Next.js projects, especially when offering multiple login methods, consider using a comprehensive [authentication solution](#examples). -```tsx filename="app/login/page.tsx" switcher -import { authenticate } from '@/app/lib/actions' +## Session Management -export default function Page() { - 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. + +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. + +### Cookie-Based Sessions + +> **🎥 Watch:** Learn more about cookie-based sessions and authentication with Next.js → [YouTube (11 minutes)](https://www.youtube.com/watch?v=DJvM2lSPn6w). + +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. + +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. + +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. + +Setting a cookie in a Next.js project might look something like this: + +**Setting 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!' }) } ``` -```jsx filename="app/login/page.jsx" switcher -import { authenticate } from '@/app/lib/actions' +```js filename="pages/api/login.js" switcher +import { serialize } from 'cookie' -export default function Page() { - return ( -
- - - -
- ) +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!' }) } ``` -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 call your Authentication Provider's API in the Server Action to handle authentication: + -```ts filename="app/lib/actions.ts" switcher +```ts filename="app/actions.ts" switcher 'use server' -import { signIn } from '@/auth' +import { cookies } from 'next/headers' -export async function authenticate(_currentState: unknown, formData: FormData) { - try { - await signIn('credentials', formData) - } catch (error) { - if (error) { - switch (error.type) { - case 'CredentialsSignin': - return 'Invalid credentials.' - default: - return 'Something went wrong.' - } - } - throw error - } +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 } ``` -```js filename="app/lib/actions.js" switcher +```js filename="app/actions.js" switcher 'use server' -import { signIn } from '@/auth' +import { cookies } from 'next/headers' -export async function authenticate(_currentState, formData) { - try { - await signIn('credentials', formData) - } catch (error) { - if (error) { - switch (error.type) { - case 'CredentialsSignin': - return 'Invalid credentials.' - default: - return 'Something went wrong.' - } - } - throw error - } +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 +} +``` + +**Accessing the session data stored in the cookie in a server component:** + +```tsx filename="app/page.tsx" switcher +import { cookies } from 'next/headers' + +export async function getSessionData(req) { + const encryptedSessionData = cookies().get('session')?.value + return encryptedSessionData ? JSON.parse(decrypt(encryptedSessionData)) : null +} +``` + +```jsx filename="app/page.jsx" switcher +import { cookies } from 'next/headers' + +export async function getSessionData(req) { + const encryptedSessionData = cookies().get('session')?.value + return encryptedSessionData ? JSON.parse(decrypt(encryptedSessionData)) : null } ``` -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: +### Database Sessions -- **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. +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. - +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. -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: +Here's a simplified example of implementing database sessions in a Next.js application: -```tsx filename="app/login/page.tsx" switcher -'use client' +**Creating a Session on the Server**: -import { authenticate } from '@/app/lib/actions' -import { useFormState, useFormStatus } from 'react-dom' + -export default function Page() { - const [errorMessage, dispatch] = useFormState(authenticate, undefined) +```ts filename="pages/api/create-session.ts" switcher +import db from '../../lib/db' +import { NextApiRequest, NextApiResponse } from 'next' - return ( -
- - -
{errorMessage &&

{errorMessage}

}
- - - ) +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(), + }) + + res.status(200).json({ sessionId }) + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }) + } } +``` -function LoginButton() { - const { pending } = useFormStatus() +```js filename="pages/api/create-session.js" switcher +import db from '../../lib/db' - return ( - - ) +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(), + }) + + res.status(200).json({ sessionId }) + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }) + } } ``` -```jsx filename="app/login/page.jsx" switcher -'use client' +
-import { authenticate } from '@/app/lib/actions' -import { useFormState, useFormStatus } from 'react-dom' + -export default function Page() { - const [errorMessage, dispatch] = useFormState(authenticate, undefined) +```js +import db from './lib/db' - return ( -
- - -
{errorMessage &&

{errorMessage}

}
- - - ) +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 +} +``` + +**Retrieving a Session in Middleware or Server-Side Logic**: + +```js +import { cookies } from 'next/headers' +import db from './lib/db' + +export async function getSession() { + const sessionId = cookies().get('sessionId')?.value + return sessionId ? await db.findSession(sessionId) : null } +``` + +
+ +### 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. + +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 + +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: + +{/* TODO: Change link to authjs.dev when new documentation is ready */} -function LoginButton() { - const { pending } = useFormStatus() +- [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/guides/upgrade-to-v5) +- [Supabase](https://supabase.com/docs/guides/getting-started/quickstarts/nextjs) +- [Stytch](https://stytch.com/docs/guides/quickstarts/nextjs) +- [Iron Session](https://github.com/vvo/iron-session) - return ( - - ) -} -``` +## Further Reading -
+To continue learning about authentication and security, check out the following resources: -For a more streamlined authentication setup in Next.js projects, especially when offering multiple login methods, consider using a comprehensive [authentication solution](#examples). +- [Understanding XSS Attacks](https://vercel.com/guides/understanding-xss-attacks) +- [Understanding CSRF Attacks](https://vercel.com/guides/understanding-csrf-attacks) ## Authorization @@ -430,7 +656,9 @@ When setting up authorization, it's important to ensure that the main security c -### Protecting API Routes +### 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. @@ -505,7 +733,7 @@ This approach, highlighted in [this security blog](/blog/security-nextjs-server- 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. -### Protecting Server Actions +#### Protecting Server Actions 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. @@ -549,7 +777,7 @@ export async function serverAction() { } ``` -### Protecting Route Handlers +#### Protecting Route Handlers 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. @@ -595,7 +823,7 @@ export async function GET() { 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. -### Authorization Using Server Components +#### Authorization Using Server Components [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. @@ -643,235 +871,6 @@ In this example, the Dashboard component renders different UIs for 'admin', 'use - **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. -## Session Management - -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. - -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. - -### Cookie-Based Sessions - -> **🎥 Watch:** Learn more about cookie-based sessions and authentication with Next.js → [YouTube (11 minutes)](https://www.youtube.com/watch?v=DJvM2lSPn6w). - -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. - -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. - -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. - -Setting a cookie in a Next.js project might look something like this: - -**Setting 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!' }) -} -``` - -```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!' }) -} -``` - - - - - -```ts filename="app/actions.ts" switcher -'use server' - -import { cookies } from 'next/headers' - -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 -} -``` - -```js filename="app/actions.js" switcher -'use server' - -import { cookies } from 'next/headers' - -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 -} -``` - -**Accessing the session data stored in the cookie in a server component:** - -```tsx filename="app/page.tsx" switcher -import { cookies } from 'next/headers' - -export async function getSessionData(req) { - const encryptedSessionData = cookies().get('session')?.value - return encryptedSessionData ? JSON.parse(decrypt(encryptedSessionData)) : null -} -``` - -```jsx filename="app/page.jsx" switcher -import { cookies } from 'next/headers' - -export async function getSessionData(req) { - const encryptedSessionData = cookies().get('session')?.value - return encryptedSessionData ? JSON.parse(decrypt(encryptedSessionData)) : null -} -``` - - - -### Database Sessions - -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. - -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. - -Here's a simplified example of implementing database sessions in a Next.js application: - -**Creating a Session on the Server**: - - - -```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 -) { - try { - const user = req.body - const sessionId = generateSessionId() - await db.insertSession({ - sessionId, - userId: user.id, - createdAt: new Date(), - }) - - res.status(200).json({ sessionId }) - } catch (error) { - res.status(500).json({ error: 'Internal Server Error' }) - } -} -``` - -```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(), - }) - - res.status(200).json({ sessionId }) - } catch (error) { - res.status(500).json({ error: 'Internal Server Error' }) - } -} -``` - - - - - -```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 -} -``` - -**Retrieving a Session in Middleware or Server-Side Logic**: - -```js -import { cookies } from 'next/headers' -import db from './lib/db' - -export async function getSession() { - const sessionId = cookies().get('sessionId')?.value - return sessionId ? await db.findSession(sessionId) : null -} -``` - - - -### 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. - -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 - -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: - -{/* TODO: Change link to authjs.dev when new documentation is ready */} - -- [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/guides/upgrade-to-v5) -- [Supabase](https://supabase.com/docs/guides/getting-started/quickstarts/nextjs) -- [Stytch](https://stytch.com/docs/guides/quickstarts/nextjs) -- [Iron Session](https://github.com/vvo/iron-session) - -## Further Reading +## FAQs -To continue learning about authentication and security, check out the following resources: - -- [Understanding XSS Attacks](https://vercel.com/guides/understanding-xss-attacks) -- [Understanding CSRF Attacks](https://vercel.com/guides/understanding-csrf-attacks) +### Why can't I handle auth in layouts? From 562eaad8f606e1ec9babed909c03732246fdb232 Mon Sep 17 00:00:00 2001 From: Delba de Oliveira Date: Thu, 21 Mar 2024 09:24:19 +0000 Subject: [PATCH 03/22] Iterate over intro --- .../09-authentication/index.mdx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 9e7bbe102be03..e4ce1dda5b66f 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 @@ -3,20 +3,20 @@ title: Authentication description: Learn how to implement authentication in Next.js. --- -This page will guide you through implementing authentication in Next.js, it covers best practices and patterns so you can choose the best solution for your application. +Authentication is a common feature in web applications, and knowing how to implement it securely is crucial for protecting your user's data and ensuring a smooth experience. This page will guide you through the Next.js features, third-party providers, and patterns you can use to implement authentication - so that you can choose the best approach for your application. -Before starting, it helps to be familiar with the following concepts: +Before starting, it helps to break down the authentication process into three key concepts or steps: -- **[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, or use a 3rd-party service like Google. - - **[Auth Providers](#auth-providers)**: Services that handle the authentication process for you. They often provide a set of helpful APIs and/or SDKs, making it easier and safer to implement auth in your application. -- **[Session Management](#session-management)**: Tracks the user's auth state (e.g. logged-in) across requests. - - **[Session Management Libraries](#session-management-libraries)**: Libraries that help you manage user sessions, including setting, storing and retrieving cookie data, and handling expiration. -- **[Authorization](#authorization)**: Decides what parts of the application the user is allowed to access. It includes redirecting users to different routes based on their auth state or role. +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, or use a 3rd-party service like Google. +2. **[Session Management](#session-management)**: Tracks the user's auth state (e.g. logged-in) across requests. It involves creating, storing, updating, and deleting sessions. +3. **[Authorization](#authorization)**: Decides what routes and data the user can access. It includes redirecting users based on their auth state or role, and preventing unauthorized access to data. -This diagram illustrates the flow, and what Next.js features you can use for authentication, session management, and authorization: +This diagram illustrates the authentication flow, and what Next.js features you can use for authentication, session management, and authorization: {/* TODO: Auth Diagram */} +> While you can implement your custom auth solution, for increased security, simplicity and better DX, we recommend using an Auth Provider. These offer pre-built solutions for authentication, session management, and authorization, as well as additional features like social logins, multi-factor authentication, and role-based access control. You can find a list of Next.js compatible providers in the [Auth Providers](#auth-providers) section. + ## Authentication From bd18e5217a6f78ff7efce978a810ba1f0c44b617 Mon Sep 17 00:00:00 2001 From: Delba de Oliveira Date: Thu, 21 Mar 2024 11:26:22 +0000 Subject: [PATCH 04/22] Write authentication section --- .../09-authentication/index.mdx | 414 ++++++++++++++---- 1 file changed, 324 insertions(+), 90 deletions(-) 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 e4ce1dda5b66f..f4e9b48014d3e 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 @@ -3,7 +3,7 @@ title: Authentication description: Learn how to implement authentication in Next.js. --- -Authentication is a common feature in web applications, and knowing how to implement it securely is crucial for protecting your user's data and ensuring a smooth experience. This page will guide you through the Next.js features, third-party providers, and patterns you can use to implement authentication - so that you can choose the best approach for your application. +Authentication is a common feature in web applications, and knowing how to implement it securely is crucial for protecting your user's data and ensuring a smooth experience. This page will guide you through the features, patterns, and third-party providers you can use to implement authentication in your Next.js application. Before starting, it helps to break down the authentication process into three key concepts or steps: @@ -15,170 +15,404 @@ This diagram illustrates the authentication flow, and what Next.js features you {/* TODO: Auth Diagram */} -> While you can implement your custom auth solution, for increased security, simplicity and better DX, we recommend using an Auth Provider. These offer pre-built solutions for authentication, session management, and authorization, as well as additional features like social logins, multi-factor authentication, and role-based access control. You can find a list of Next.js compatible providers in the [Auth Providers](#auth-providers) section. +> While you can implement your custom auth solution, for increased security, simplicity and better DX, we recommend using an auth provider. These offer pre-built solutions for authentication, session management, and authorization, as well as additional features like social logins, multi-factor authentication, and role-based access control. You can find a list of Next.js compatible providers in the [Auth Providers](#auth-providers) section. ## Authentication +For educational purposes, the examples in this section will walk you through basic username and password authentication. However, you can replace this with your preferred authentication provider. + ### Sign-up and login functionality -You can use [Server Actions](/docs/app/building-your-application/rendering/server-components) with React [`
`](https://react.dev/reference/react-dom/components/form) to create login or sign-up UI. Since Server Actions execute on the server, they provide a secure environment for handling sensitive operations. +You can use React's [``](https://react.dev/reference/react-dom/components/form), [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 data, validate form fields on the server, and call your Authentication Provider's API or database. Since Server Actions execute on the server, they provide a secure environment for handling sensitive operations. -1. The user submits their credentials through a login or registration form. -2. The form invokes a Server Action, which calls your authentication provider's API to verify the user's credentials, and manage the user's session. -3. If verification is successful, the user is authenticated and redirected to a protected route. -4. If verification is unsuccessful, an error message is shown. +Here are the steps to implement a sign-up and/or login form: -For example, consider a login form where users can enter their credentials: +#### 1. Capture user credentials -```tsx filename="app/login/page.tsx" switcher -import { login } from '@/app/lib/auth' +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: -export default function Page() { +```tsx filename="app/ui/signup-form.tsx" switcher +import { signup } from '@/app/actions/auth' + +export function SignupForm() { return ( - - - - + +
+ + +
+
+ + +
+
+ + +
+
) } ``` -```jsx filename="app/login/page.jsx" switcher -import { login } from '@/app/lib/auth' +```jsx filename="app/ui/signup-form.js" switcher +import { signup } from '@/app/actions/auth' -export default function Page() { +export function SignupForm() { return ( -
- - s + +
+ + +
+
+ + +
+
+ + +
+
) } ``` -The form above has two input fields for capturing the user's email and password. On submission, it calls the `login` Server Action. +```tsx filename="app/actions/auth.tsx" switcher +export async function signup(formData: FormData) { + // ... +} +``` -Inside the Server Action, you can do the following: +```jsx filename="app/actions/auth.js" switcher +export async function signup(formData) { + // ... +} +``` -1. Extract and validate the form input values. For example, check if the email is valid and the password is not empty. -2. Call your authentication provider's API to verify the user's credentials. -3. If the credentials are valid, create a session for the user that persists across requests. See [Session Management](#session-management) for more details. +#### 2. Validate form fields on the server -```ts filename="app/lib/actions.ts" switcher -'use server' +Use the Server Action to validate the form fields on the the server and prevent client-side tampering. If your authentication provider doesn't provide field validation (or verification), you can use a schema validation library like [Zod](https://zod.dev/) or [Yup](https://github.com/jquense/yup). -import { signIn } from 'auth-provider' +Normal form 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, and name is not empty. Using Zod as an example, you can define a schema for your form fields: -export async function login(formData: FormData) { - try { - await signIn('credentials', formData) - redirect('/dashboard') - } catch (error) { - if (error) { - switch (error.type) { - case 'CredentialsSignin': - return 'Invalid credentials.' - default: - return 'Something went wrong.' +```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 } - throw error - } -} + | undefined ``` -```js filename="app/lib/actions.js" switcher -'use server' +```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(), +}) +``` -import { signIn } from 'auth-provider' +Then, in your Server Action, after validation, return early if any form fields do not match the criteria. This will prevent unecessary calls to your authentication provider's API or database: -export async function authenticate(formData) { - try { - await signIn('credentials', formData) - } catch (error) { - if (error) { - switch (error.type) { - case 'CredentialsSignin': - return 'Invalid credentials.' - default: - return 'Something went wrong.' - } +```tsx filename="app/actions/auth.tsx" 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, } - throw error } + + // TODO: Call provider or db to create user... } ``` -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: +Back in your ``, you can use React's `useFormState()` hook to conditionally display validation errors to the user: -```tsx filename="app/login/page.tsx" switcher +```tsx filename="app/ui/signup-form.tsx" switcher highlight={6,14,20,26-35} 'use client' -import { authenticate } from '@/app/lib/actions' -import { useFormState, useFormStatus } from 'react-dom' +import { useFormState } from 'react-dom' -export default function Page() { - const [errorMessage, dispatch] = useFormState(authenticate, undefined) +export function SignupForm() { + const [state, action] = useFormState(signup, undefined) return ( -
- - -
{errorMessage &&

{errorMessage}

}
- + +
+ + +
+ {state?.errors?.name &&

{state.errors.name}

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

{state.errors.email}

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

Password must:

+
    + {state.errors.password.map((error) => ( +
  • - {error}
  • + ))} +
+
+ )} + ) } +``` -function LoginButton() { - const { pending } = useFormStatus() +```jsx filename="app/ui/signup-form.js" switcher highlight={6,14,20,26-35} +'use client' + +import { useFormState } from 'react-dom' + +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/login/page.jsx" switcher +> **Tip:** Remove the `required` attribute from the input fields to allow `useFormState()` to handle validation. `required` uses the browser's native form validation, and will prevent the form from being submitted if the field is empty. + +You can also use the `useFormStatus()` hook to handle the pending state of the form on submission: + +```tsx filename="app/ui/signup-form.tsx" switcher 'use client' -import { authenticate } from '@/app/lib/actions' -import { useFormState, useFormStatus } from 'react-dom' +import { useFormStatus, useFormState } from 'react-dom' -export default function Page() { - const [errorMessage, dispatch] = useFormState(authenticate, undefined) +// Component + +export function SignupButton() { + const { pending } = useFormStatus() return ( -
- - -
{errorMessage &&

{errorMessage}

}
- - + ) } +``` + +```jsx filename="app/ui/signup-form.js" switcher +'use client' + +import { useFormStatus, useFormState } from 'react-dom' -function LoginButton() { +// Component + +export function SignupButton() { const { pending } = useFormStatus() return ( ) } ``` -### Logout functionality +> **Tip:** `useFormStatus()` should be used in a separate component to avoid re-rendering the entire form when the form status changes. + +#### 3. Create or check user credentials + +After validating the form fields, you can call your authentication provider's API or database to create a new user account or check if the user exists. + +Continuing from the previous example, you should 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 Provider'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 +} +``` + +```jsx filename="app/actions/auth.js" switcher +// ... + +export async function signup(state, 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 Provider'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 +} +``` -### Auth Providers +> **Tips:** +> +> - Inversely, on login, you should check if the hashed password matches the user's input. +> - 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. Consider libraries such as [use-debounce](https://www.npmjs.com/package/use-debounce) to manage the frequency of these checks. +> - The example above breaks down the authentication steps for the purpose of teaching, making it verbose. This highlights that implementing your own secure auth solution can quickly become complex and verbose too. Consider using an [Auth Provider](#auth-provider) to simplify the process. -The following Auth Providers offer a range of authentication solutions for Next.js applications: +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.
From 84fd79dfcb8ecfd9ea90eb56b711fa9185ee53ee Mon Sep 17 00:00:00 2001 From: Delba de Oliveira Date: Thu, 21 Mar 2024 12:15:54 +0000 Subject: [PATCH 05/22] Write session management section (wip) --- .../09-authentication/index.mdx | 227 ++++++++++-------- 1 file changed, 122 insertions(+), 105 deletions(-) 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 f4e9b48014d3e..478d8263fe20f 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 @@ -3,7 +3,7 @@ title: Authentication description: Learn how to implement authentication in Next.js. --- -Authentication is a common feature in web applications, and knowing how to implement it securely is crucial for protecting your user's data and ensuring a smooth experience. This page will guide you through the features, patterns, and third-party providers you can use to implement authentication in your Next.js application. +Authentication is a common feature in web applications, and knowing how to implement it securely is crucial for protecting your user's data and ensuring a smooth experience. This page will guide you through the best practices, Next.js and React features, and third-party libraries you can use to implement authentication in your application. Before starting, it helps to break down the authentication process into three key concepts or steps: @@ -354,7 +354,9 @@ export async function signup(state: FormState, formData: FormData) { } } - // TODO: 4. Create user session + // TODO: + // 4. Create user session + // 5. Redirect user } ``` @@ -402,7 +404,9 @@ export async function signup(state, formData) { } } - // TODO: 4. Create user session + // TODO: + // 4. Create user session + // 5. Redirect user } ``` @@ -547,76 +551,50 @@ export default async function handler(req, res) {
-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: - -- **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. - -For a more streamlined authentication setup in Next.js projects, especially when offering multiple login methods, consider using a comprehensive [authentication solution](#examples). - ## Session Management -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. +Session management ensures that an user's authenticated state is preserved across requests, and sometimes, devices. This makes it more convenient for users as it prevents the need for repeated logins. -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. +There are two types of sessions: -### Cookie-Based Sessions +1. **Stateless (or Optimistic) Sessions**: Store session data in browser cookies. The cookie is sent with each request, and the is session verified on the server. This method is simpler, but can be less secure. +2. **Database (or Secure) Sessions**: Store session data on the server, with the user's browser only receiving the encrypted session ID. This method is more secure, but can be more complex and use more server resources. -> **🎥 Watch:** Learn more about cookie-based sessions and authentication with Next.js → [YouTube (11 minutes)](https://www.youtube.com/watch?v=DJvM2lSPn6w). +> While you can use either method, or a combination of both, we recommend using a third-party library for session management. See [Session Management Libraries](#session-management-libraries) for more information. -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. +### Stateless Sessions -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. + -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. +You can use Next.js' [`cookies()`](/docs/app/api-reference/functions/cookies) API to store session data in the browser. But, before setting the cookies, you should: -Setting a cookie in a Next.js project might look something like this: +1. Generate a secret key to sign the session data, and store it in your [environment variables file](/docs/app/building-your-application/configuring/environment-variables). +2. Encrypt your key and session to prevent tampering. +3. Set the cookies using the recommended options. -**Setting a cookie on the server:** +In addition to the above, consider introducing functionality to manage session data, such as decrypting, extending, and deleting sessions. - +> **Tip:** If you're using a [Auth Provider](#auth-provider), check if they handle session management. If they do, you can skip this section. -```ts filename="pages/api/login.ts" switcher -import { serialize } from 'cookie' -import type { NextApiRequest, NextApiResponse } from 'next' +Continuing from the previous example, we'll demonstrate how you manage a sessions using [Jose](https://www.npmjs.com/package/jose) (a popular session management library), and set the cookie on the server using Next.js. -export default function handler(req: NextApiRequest, res: NextApiResponse) { - const sessionData = req.body - const encryptedSessionData = encrypt(sessionData) +#### 1. Generating a secret key - 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!' }) -} -``` +#### 2. Encrypting the session -```js filename="pages/api/login.js" switcher -import { serialize } from 'cookie' +#### 3. Setting the cookie (recommended options) -export default function handler(req, res) { - const sessionData = req.body - const encryptedSessionData = encrypt(sessionData) +#### Decrypting the session data - 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!' }) -} -``` +#### Updating the session - +#### Deleting the session - +> **Tips**: +> +> - Use the React's [`server-only`](https://www.npmjs.com/package/server-only) package to ensure that session management logic is only executed on the server. +> - For improved security, cookies should always be set on the server. This prevents client-side tampering and ensures that sensitive data is not exposed to the client. +> - 🎥 Watch: Learn more about stateless sessions and authentication with Next.js → [YouTube (11 minutes)](https://www.youtube.com/watch?v=DJvM2lSPn6w). ```ts filename="app/actions.ts" switcher 'use server' @@ -674,6 +652,49 @@ export async function getSessionData(req) { + + +**Setting 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!' }) +} +``` + +```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!' }) +} +``` + + + ### Database Sessions 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. @@ -682,10 +703,36 @@ However, this approach has its tradeoffs. It can increase performance overhead d Here's a simplified example of implementing database sessions in a Next.js application: -**Creating a Session on the Server**: + + +```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 +} +``` + +**Retrieving a Session in Middleware or Server-Side Logic**: + +```js +import { cookies } from 'next/headers' +import db from './lib/db' + +export async function getSession() { + const sessionId = cookies().get('sessionId')?.value + return sessionId ? await db.findSession(sessionId) : null +} +``` + + +**Creating a Session on the Server**: + ```ts filename="pages/api/create-session.ts" switcher import db from '../../lib/db' import { NextApiRequest, NextApiResponse } from 'next' @@ -732,32 +779,6 @@ export default async function handler(req, res) { - - -```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 -} -``` - -**Retrieving a Session in Middleware or Server-Side Logic**: - -```js -import { cookies } from 'next/headers' -import db from './lib/db' - -export async function getSession() { - const sessionId = cookies().get('sessionId')?.value - return sessionId ? await db.findSession(sessionId) : null -} -``` - - - ### 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. @@ -766,28 +787,6 @@ With [authentication solutions](#examples) such as [NextAuth.js](https://authjs. 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 - -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: - -{/* TODO: Change link to authjs.dev when new documentation is ready */} - -- [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/guides/upgrade-to-v5) -- [Supabase](https://supabase.com/docs/guides/getting-started/quickstarts/nextjs) -- [Stytch](https://stytch.com/docs/guides/quickstarts/nextjs) -- [Iron Session](https://github.com/vvo/iron-session) - -## Further Reading - -To continue learning about authentication and security, check out the following resources: - -- [Understanding XSS Attacks](https://vercel.com/guides/understanding-xss-attacks) -- [Understanding CSRF Attacks](https://vercel.com/guides/understanding-csrf-attacks) - ## Authorization 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. @@ -1105,6 +1104,24 @@ In this example, the Dashboard component renders different UIs for 'admin', 'use - **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. -## FAQs +## Auth Providers + +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: + +{/* TODO: Change link to authjs.dev when new documentation is ready */} + +- [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/guides/upgrade-to-v5) +- [Supabase](https://supabase.com/docs/guides/getting-started/quickstarts/nextjs) +- [Stytch](https://stytch.com/docs/guides/quickstarts/nextjs) +- [Iron Session](https://github.com/vvo/iron-session) + +## Further Reading + +To continue learning about authentication and security, check out the following resources: -### Why can't I handle auth in layouts? +- [Understanding XSS Attacks](https://vercel.com/guides/understanding-xss-attacks) +- [Understanding CSRF Attacks](https://vercel.com/guides/understanding-csrf-attacks) From be14c2d6e99f21d07c27b2413377e676686e7ad0 Mon Sep 17 00:00:00 2001 From: Delba de Oliveira Date: Thu, 21 Mar 2024 17:07:56 +0000 Subject: [PATCH 06/22] Continue working on sessions section (wip) --- .../09-authentication/index.mdx | 317 ++++++++++++------ 1 file changed, 221 insertions(+), 96 deletions(-) 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 478d8263fe20f..0c3c146f0e7cb 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,17 +1,17 @@ --- title: Authentication -description: Learn how to implement authentication in Next.js. +description: Learn how to implement authentication in your Next.js application. --- -Authentication is a common feature in web applications, and knowing how to implement it securely is crucial for protecting your user's data and ensuring a smooth experience. This page will guide you through the best practices, Next.js and React features, and third-party libraries you can use to implement authentication in your application. +Authentication is a feature common to many web applications, and understanding how to implement it is crucial to protect data and ensure a smooth user experience. This page will guide you through how you can use Next.js and React features with third-party libraries to implement authentication. It also covers best practices and patterns so you can choose the right approach for your application. -Before starting, it helps to break down the authentication process into three key concepts or steps: +Before starting, it helps to break down the authentication process into three key concepts, or steps: 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, or use a 3rd-party service like Google. 2. **[Session Management](#session-management)**: Tracks the user's auth state (e.g. logged-in) across requests. It involves creating, storing, updating, and deleting sessions. 3. **[Authorization](#authorization)**: Decides what routes and data the user can access. It includes redirecting users based on their auth state or role, and preventing unauthorized access to data. -This diagram illustrates the authentication flow, and what Next.js features you can use for authentication, session management, and authorization: +This diagram illustrates the authentication flow in Next.js: {/* TODO: Auth Diagram */} @@ -25,7 +25,7 @@ For educational purposes, the examples in this section will walk you through bas ### Sign-up and login functionality -You can use React's [`
`](https://react.dev/reference/react-dom/components/form), [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 data, validate form fields on the server, and call your Authentication Provider's API or database. Since Server Actions execute on the server, they provide a secure environment for handling sensitive operations. +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 data, validate form fields on the server, and call your Authentication Provider's API or database. Since Server Actions execute on the server, they provide a secure environment for handling sensitive data. Here are the steps to implement a sign-up and/or login form: @@ -95,9 +95,9 @@ export async function signup(formData) { #### 2. Validate form fields on the server -Use the Server Action to validate the form fields on the the server and prevent client-side tampering. If your authentication provider doesn't provide field validation (or verification), you can use a schema validation library like [Zod](https://zod.dev/) or [Yup](https://github.com/jquense/yup). +Use the Server Action tos validate the form fields on the 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 form 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, and name is not empty. Using Zod as an example, you can define a schema for your form fields: +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 schema for your form with appropriate error messages: ```ts filename="app/lib/definitions.ts" switcher import { z } from 'zod' @@ -152,7 +152,7 @@ export const SignupFormSchema = z.object({ }) ``` -Then, in your Server Action, after validation, return early if any form fields do not match the criteria. This will prevent unecessary calls to your authentication provider's API or database: +Then, in your Server Action, after validation, return early if any form fields do not match the criteria. By returning early, you prevent unecessary calls to your authentication provider's API or database: ```tsx filename="app/actions/auth.tsx" switcher import { SignupFormSchema, FormState } from '@/app/lib/definitions' @@ -262,9 +262,9 @@ export function SignupForm() { } ``` -> **Tip:** Remove the `required` attribute from the input fields to allow `useFormState()` to handle validation. `required` uses the browser's native form validation, and will prevent the form from being submitted if the field is empty. +> **Tip:** Remove the `required` attribute from the input fields to allow `useFormState()` to handle validation. `required` uses the browser's native form validation, and will prevent `useFormState()` from showing validation errors. -You can also use the `useFormStatus()` hook to handle the pending state of the form on submission: +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' @@ -304,15 +304,13 @@ export function SignupButton() { > **Tip:** `useFormStatus()` should be used in a separate component to avoid re-rendering the entire form when the form status changes. -#### 3. Create or check user credentials +#### 3. Create user or check user credentials -After validating the form fields, you can call your authentication provider's API or database to create a new user account or check if the user exists. +After validating the form fields, 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, you should 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 // ... @@ -361,8 +359,6 @@ export async function signup(state: FormState, formData: FormData) { ``` ```jsx filename="app/actions/auth.js" switcher -// ... - export async function signup(state, formData) { // 1. Validate form fields // ... @@ -410,14 +406,15 @@ export async function signup(state, formData) { } ``` +Inversely, on login, you should check if the hashed password matches the user's input. + +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. + > **Tips:** > -> - Inversely, on login, you should check if the hashed password matches the user's input. > - 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. Consider libraries such as [use-debounce](https://www.npmjs.com/package/use-debounce) to manage the frequency of these checks. > - The example above breaks down the authentication steps for the purpose of teaching, making it verbose. This highlights that implementing your own secure auth solution can quickly become complex and verbose too. Consider using an [Auth Provider](#auth-provider) to simplify the process. -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. - @@ -553,100 +550,212 @@ export default async function handler(req, res) { ## Session Management -Session management ensures that an user's authenticated state is preserved across requests, and sometimes, devices. This makes it more convenient for users as it prevents the need for repeated logins. +Session management ensures that an user's authenticated state is preserved across requests, and sometimes, across multiple devices. It involves creating, storing, updating, and deleting sessions. There are two types of sessions: -1. **Stateless (or Optimistic) Sessions**: Store session data in browser cookies. The cookie is sent with each request, and the is session verified on the server. This method is simpler, but can be less secure. -2. **Database (or Secure) Sessions**: Store session data on the server, with the user's browser only receiving the encrypted session ID. This method is more secure, but can be more complex and use more server resources. +1. **Stateless (or Optimistic) Sessions**: Session data is stored in browser's cookies. The cookie is sent with each request, allowing the session verified to be verified on the server. This method is simpler, but can be less secure if not implemented correctly. +2. **Database (or Secure) Sessions**: Session data is stored on a databases, with the user's browser only receiving the encrypted session ID. This method is more secure, but can be more complex and use more server resources. -> While you can use either method, or a combination of both, we recommend using a third-party library for session management. See [Session Management Libraries](#session-management-libraries) for more information. +> While you can use either method, or both, we recommend using a third-party library for session management. See [Session Management Libraries](#session-management-libraries) for more information. ### Stateless Sessions -You can use Next.js' [`cookies()`](/docs/app/api-reference/functions/cookies) API to store session data in the browser. But, before setting the cookies, you should: - -1. Generate a secret key to sign the session data, and store it in your [environment variables file](/docs/app/building-your-application/configuring/environment-variables). -2. Encrypt your key and session to prevent tampering. -3. Set the cookies using the recommended options. +To create and manage stateless sessions in your Next.js application, there are a few steps you need to follow: -In addition to the above, consider introducing functionality to manage session data, such as decrypting, extending, and deleting sessions. +1. Generate a secret key, which will be used to signed your session, and store the key as an [environment variable](/docs/app/building-your-application/configuring/environment-variables). +2. Encrypt/decrypt session data using a session management library. +3. Save the session as 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. -> **Tip:** If you're using a [Auth Provider](#auth-provider), check if they handle session management. If they do, you can skip this section. +In addition to the above, consider adding functionality update (or extend) the session when the user returns to the application, and delete the session when the user logs out. -Continuing from the previous example, we'll demonstrate how you manage a sessions using [Jose](https://www.npmjs.com/package/jose) (a popular session management library), and set the cookie on the server using Next.js. +> **Tip:** +> +> - If you're using a [Auth Provider](#auth-provider), check if they provide session management. +> - Here, we're using sessions, but the same principles apply to tokens. #### 1. Generating a secret key -#### 2. Encrypting the session +You'll need a secret key to sign the session data. There are a few ways you can generate this key. For example, you may choose to use the `openssl` command in your terminal: -#### 3. Setting the cookie (recommended options) +```bash filename="terminal" +openssl rand -base64 32 +``` -#### Decrypting the session data +This command generates a 32-character random string that you can use as your secret key. Store this key in your environment variables file: -#### Updating the session +```bash filename=".env" +SESSION_SECRET=your_secret_key +``` -#### Deleting the session +You can then reference this key in your session management logic: -> **Tips**: +```tsx filename="app/actions/session.ts" switcher +const secretKey = process.env.SESSION_SECRET +``` + +#### 2. Encrypting and decrypting sessions + +Next, you can use your preferred [session management library](#session-management-libraries) to encrypt and decrypt your session. Continuing from the previous example, we'll use [Jose](https://www.npmjs.com/package/jose) (which is compatible with the Edge Runtime) to demonstrate how to encrypt and decrypt session data: + +```tsx filename="app/actions/session.ts" switcher +import 'server-only' +import { SignJWT, jwtVerify } from 'jose' + +const secretKey = process.env.SESSION_SECRET +const encodedKey = new TextEncoder().encode(secretKey) + +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 { + const { payload } = await jwtVerify(session, encodedKey, { + algorithms: ['HS256'], + }) + return payload + } catch (error) { + console.log('Failed to verify session') + } +} +``` + +```jsx filename="app/actions/session.js" switcher +import 'server-only' +import { SignJWT, jwtVerify } from 'jose' + +const secretKey = process.env.SESSION_SECRET +const encodedKey = new TextEncoder().encode(secretKey) + +export async function encrypt(payload) { + return new SignJWT(payload) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('7d') // For database sessions + .sign(encodedKey) +} + +export async function decrypt(session) { + try { + const { payload } = await jwtVerify(session, encodedKey, { + algorithms: ['HS256'], + }) + return payload + } catch (error) { + console.log('Failed to verify session') + } +} +``` + +> **Tip**: > -> - Use the React's [`server-only`](https://www.npmjs.com/package/server-only) package to ensure that session management logic is only executed on the server. -> - For improved security, cookies should always be set on the server. This prevents client-side tampering and ensures that sensitive data is not exposed to the client. -> - 🎥 Watch: Learn more about stateless sessions and authentication with Next.js → [YouTube (11 minutes)](https://www.youtube.com/watch?v=DJvM2lSPn6w). +> - The payload should contain the **minimum**, unique user data that'll be used in subsequent requests, such as the user's ID, role, session expiration date, etc. It should not contain sensitive data like passwords, phone numbers, or credit card information. -```ts filename="app/actions.ts" switcher -'use server' +#### 3. Setting the cookie (recommended options) +To set the session as 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: + +- **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. + +Please refer to [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) for more information on cookie options. + +Continuing from the previous example, here's an example of how you'd save the session information as cookie: + +```tsx filename="app/actions/session.ts" switcher +import 'server-only' import { cookies } from 'next/headers' -export async function handleLogin(sessionData) { - const encryptedSessionData = encrypt(sessionData) // Encrypt your session data - cookies().set('session', encryptedSessionData, { +// Decrypt and encrypt functions + +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: process.env.NODE_ENV === 'production', - maxAge: 60 * 60 * 24 * 7, // One week + secure: true, + expires: expiresAt, + sameSite: 'lax', path: '/', }) - // Redirect or handle the response after setting the cookie } ``` -```js filename="app/actions.js" switcher -'use server' +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 { cookies } from 'next/headers' +```tsx filename="app/actions/auth.ts" switcher +import { createSession } from '@/app/actions/session' -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 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 Auth Provider's API + + // Current steps: + // 4. Create user session + await createSession(user.id) + // 5. Redirect user + redirect('/profile') } ``` -**Accessing the session data stored in the cookie in a server component:** +> **Tips**: +> +> - For improved security, cookies should always be set on the server to prevent client-side tampering. +> - Use the React's [`server-only`](https://www.npmjs.com/package/server-only) package to ensure that your session management utilities are only executed on the server. +> - 🎥 Watch: Learn more about stateless sessions and authentication with Next.js → [YouTube (11 minutes)](https://www.youtube.com/watch?v=DJvM2lSPn6w). -```tsx filename="app/page.tsx" switcher +#### Updating the session + +You can also extend the session's expiration time. This is useful for keeping the user logged in after they access the appplication again. For example: + +```tsx filename="app/actions/session.ts" switcher +import 'server-only' import { cookies } from 'next/headers' -export async function getSessionData(req) { - const encryptedSessionData = cookies().get('session')?.value - return encryptedSessionData ? JSON.parse(decrypt(encryptedSessionData)) : null +// Decrypt and encrypt utils + +export async function updateSession() { + const session = cookies().get('session')?.value + if (!session) return + + // Refresh the session + const parsed = await decrypt(session) + parsed.expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) + + cookies.set({ + name: 'session', + value: await encrypt(parsed), + httpOnly: true, + expires: parsed.expires, + }) } ``` -```jsx filename="app/page.jsx" switcher +#### Deleting the session + +To delete the cookie, you can clear the cookie on the server: + +```tsx filename="app/actions/session.ts" switcher +import 'server-only' import { cookies } from 'next/headers' -export async function getSessionData(req) { - const encryptedSessionData = cookies().get('session')?.value - return encryptedSessionData ? JSON.parse(decrypt(encryptedSessionData)) : null +// Other session management utils + +export async function deleteSession() { + cookies().delete('session') } ``` @@ -697,36 +806,55 @@ export default function handler(req, res) { ### Database Sessions -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. - -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. +To create and manage database sessions in your Next.js application, you'll need to follow these steps: -Here's a simplified example of implementing database sessions in a Next.js application: +1. Create a table in your database to store session data. +2. Implement functionality to insert, find, 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 checks in [Middleware](#middleware)) -```js -import db from './lib/db' +Here's a breakdown of how you can create a new database session, and encrypt the session ID before storing it in a cookie: -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 -} -``` +```tsx +import cookies from 'next/headers' -**Retrieving a Session in Middleware or Server-Side Logic**: +// Decrypt and encrypt utils -```js -import { cookies } from 'next/headers' -import db from './lib/db' +export async function createSession(id: number) { + 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 }) -export async function getSession() { - const sessionId = cookies().get('sessionId')?.value - return sessionId ? await db.findSession(sessionId) : null + 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. +> - Database sessions can be used to keep track of user activity, such as 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, or view their active sessions. + @@ -779,17 +907,14 @@ export default async function handler(req, res) { -### 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. +## Authorization -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. +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. -## Authorization +There are two main types of authorization: -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. +1. **Role-Based Authorization**: Users are assigned roles that determine what they can access. For example, an admin user might have access to all parts of the application, while a regular user might only have access to their profile. +2. **Permission-Based Authorization**: Users are assigned permissions that determine what they can access. For example, a user might have permission to view their profile, but not edit it. ### Protecting Routes with Middleware From 5aca67bfd77e11973bdcb25ab49cfd066a811066 Mon Sep 17 00:00:00 2001 From: Delba de Oliveira Date: Fri, 22 Mar 2024 09:56:24 +0000 Subject: [PATCH 07/22] Fresh eyes, iterate --- .../09-authentication/index.mdx | 84 +++++++++---------- 1 file changed, 39 insertions(+), 45 deletions(-) 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 0c3c146f0e7cb..a17b6e8e14002 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 @@ -3,29 +3,29 @@ title: Authentication description: Learn how to implement authentication in your Next.js application. --- -Authentication is a feature common to many web applications, and understanding how to implement it is crucial to protect data and ensure a smooth user experience. This page will guide you through how you can use Next.js and React features with third-party libraries to implement authentication. It also covers best practices and patterns so you can choose the right approach for your application. +Understanding how to implement authentication is crucial to protect your application's data and ensure a smooth user experience. This page will guide you through the best practices, Next.js and React features, and third-party libraries you can use, so you can choose the right approach for your application. Before starting, it helps to break down the authentication process into three key concepts, or steps: -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, or use a 3rd-party service like Google. -2. **[Session Management](#session-management)**: Tracks the user's auth state (e.g. logged-in) across requests. It involves creating, storing, updating, and deleting sessions. -3. **[Authorization](#authorization)**: Decides what routes and data the user can access. It includes redirecting users based on their auth state or role, and preventing unauthorized access to data. +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 (e.g. logged-in) across requests. +3. **[Authorization](#authorization)**: Decides what routes and data the user can access. -This diagram illustrates the authentication flow in Next.js: +This diagram provides an overview of the authentication flow in Next.js: {/* TODO: Auth Diagram */} -> While you can implement your custom auth solution, for increased security, simplicity and better DX, we recommend using an auth provider. These offer pre-built solutions for authentication, session management, and authorization, as well as additional features like social logins, multi-factor authentication, and role-based access control. You can find a list of Next.js compatible providers in the [Auth Providers](#auth-providers) section. - ## Authentication -For educational purposes, the examples in this section will walk you through basic username and password authentication. However, you can replace this with your preferred authentication provider. +For educational purposes, the examples in this section will walk you through basic username and password authentication. + +> While you can implement your custom auth solution, for increased security, simplicity and better DX, we recommend using an auth provider. These offer pre-built solutions for authentication, session management, and authorization, as well as additional features like social logins, multi-factor authentication, and role-based access control. You can find a list of Next.js compatible providers in the [Auth Providers](#auth-providers) section. ### 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 data, validate form fields on the server, and call your Authentication Provider's API or database. Since Server Actions execute on the server, they provide a secure environment for handling sensitive data. +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 data, validate form fields, and call your Authentication Provider's API or database. Since Server Actions execute on the server, they provide a secure environment for handling sensitive data. Here are the steps to implement a sign-up and/or login form: @@ -412,7 +412,7 @@ After successfully creating the user account or verifying the user, you can crea > **Tips:** > -> - 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. Consider libraries such as [use-debounce](https://www.npmjs.com/package/use-debounce) to manage the frequency of these checks. +> - 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. Consider debouncing requests with libraries such as [use-debounce](https://www.npmjs.com/package/use-debounce) to manage the frequency of these checks. > - The example above breaks down the authentication steps for the purpose of teaching, making it verbose. This highlights that implementing your own secure auth solution can quickly become complex and verbose too. Consider using an [Auth Provider](#auth-provider) to simplify the process. @@ -550,12 +550,12 @@ export default async function handler(req, res) { ## Session Management -Session management ensures that an user's authenticated state is preserved across requests, and sometimes, across multiple devices. It involves creating, storing, updating, and deleting sessions. +Session management ensures that an user's authenticated state is preserved across requests, and sometimes, devices. It involves creating, storing, updating, and deleting sessions. There are two types of sessions: -1. **Stateless (or Optimistic) Sessions**: Session data is stored in browser's cookies. The cookie is sent with each request, allowing the session verified to be verified on the server. This method is simpler, but can be less secure if not implemented correctly. -2. **Database (or Secure) Sessions**: Session data is stored on a databases, with the user's browser only receiving the encrypted session ID. This method is more secure, but can be more complex and use more server resources. +1. **Stateless Sessions**: Session data is stored in 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 Sessions**: Session data is stored on a databases, with the user's browser only receiving the encrypted session ID. This method is more secure, but can be more complex and use more server resources. > While you can use either method, or both, we recommend using a third-party library for session management. See [Session Management Libraries](#session-management-libraries) for more information. @@ -569,12 +569,12 @@ To create and manage stateless sessions in your Next.js application, there are a 2. Encrypt/decrypt session data using a session management library. 3. Save the session as 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. -In addition to the above, consider adding functionality update (or extend) the session when the user returns to the application, and delete the session when the user logs out. +In addition to the above, consider adding functionality [update (or extend)](#updating-the-session) the session when the user returns to the application, and [delete](#deleting-the-session) the session when the user logs out. > **Tip:** > -> - If you're using a [Auth Provider](#auth-provider), check if they provide session management. -> - Here, we're using sessions, but the same principles apply to tokens. +> - If you're using a [Auth Provider](#auth-provider), check if they handle sessions. +> - Although the docs mention session, the same principles apply to tokens. #### 1. Generating a secret key @@ -598,7 +598,7 @@ const secretKey = process.env.SESSION_SECRET #### 2. Encrypting and decrypting sessions -Next, you can use your preferred [session management library](#session-management-libraries) to encrypt and decrypt your session. Continuing from the previous example, we'll use [Jose](https://www.npmjs.com/package/jose) (which is compatible with the Edge Runtime) to demonstrate how to encrypt and decrypt session data: +Next, you can use your preferred [session management library](#session-management-libraries) to encrypt and decrypt your session. Continuing from the previous example, we'll use [Jose](https://www.npmjs.com/package/jose) (compatible with the Edge Runtime) to demonstrate how to encrypt and decrypt session data: ```tsx filename="app/actions/session.ts" switcher import 'server-only' @@ -654,21 +654,22 @@ export async function decrypt(session) { } ``` -> **Tip**: +> **Tips**: > -> - The payload should contain the **minimum**, unique user data that'll be used in subsequent requests, such as the user's ID, role, session expiration date, etc. It should not contain sensitive data like passwords, phone numbers, or credit card information. +> - Use the 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. +> - The payload should 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, or credit card information, or sensitive data like passwords. #### 3. Setting the cookie (recommended options) -To set the session as 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: +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: - **HttpOnly**: Prevents client-side JavaScript from accessing the cookie. -- **Secure**: Use HTTPS to send 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. -Please refer to [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) for more information on cookie options. +Please refer to [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) for more information on each of these options. Continuing from the previous example, here's an example of how you'd save the session information as cookie: @@ -676,8 +677,6 @@ Continuing from the previous example, here's an example of how you'd save the se import 'server-only' import { cookies } from 'next/headers' -// Decrypt and encrypt functions - 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 }) @@ -713,47 +712,42 @@ export async function signup(state: FormState, formData: FormData) { > **Tips**: > -> - For improved security, cookies should always be set on the server to prevent client-side tampering. -> - Use the React's [`server-only`](https://www.npmjs.com/package/server-only) package to ensure that your session management utilities are only executed on the server. +> - For improved security, **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). -#### Updating the session +#### Updating (or extending) the session -You can also extend the session's expiration time. This is useful for keeping the user logged in after they access the appplication again. For example: +You can also extend the session's expiration time. This is useful for keeping the user logged in after they access the appplication again. Ensure you decrypt the session first to make sure it's valid. For example: ```tsx filename="app/actions/session.ts" switcher import 'server-only' import { cookies } from 'next/headers' -// Decrypt and encrypt utils - export async function updateSession() { const session = cookies().get('session')?.value if (!session) return - // Refresh the session const parsed = await decrypt(session) parsed.expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) - cookies.set({ - name: 'session', - value: await encrypt(parsed), + cookies().set('session', session, { httpOnly: true, + secure: true, expires: parsed.expires, + sameSite: 'lax', + path: '/', }) } ``` #### Deleting the session -To delete the cookie, you can clear the cookie on the server: +To delete the session, you can clear the cookie: ```tsx filename="app/actions/session.ts" switcher import 'server-only' import { cookies } from 'next/headers' -// Other session management utils - export async function deleteSession() { cookies().delete('session') } @@ -808,19 +802,17 @@ export default function handler(req, res) { To create and manage database sessions in your Next.js application, you'll need to follow these steps: -1. Create a table in your database to store session data. +1. Create a table in your database to store session data (or check if your Auth Provider handles this). 2. Implement functionality to insert, find, 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 checks in [Middleware](#middleware)) +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](#middleware)) -Here's a breakdown of how you can create a new database session, and encrypt the session ID before storing it in a cookie: +Here's an example of how you can create a new database session, and encrypt the session ID before storing it in a cookie: ```tsx import cookies from 'next/headers' -// Decrypt and encrypt utils - export async function createSession(id: number) { const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) @@ -855,6 +847,8 @@ export async function createSession(id: number) { > - 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. > - Database sessions can be used to keep track of user activity, such as 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, or view their active sessions. +After implementing session management, you'll need to add authorization logic to allow you control what users can access and do within your application. Continue to the [Authorization](#authorization) section to learn more. + @@ -911,10 +905,10 @@ export default async function handler(req, res) { 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. -There are two main types of authorization: +There are three main types of authorization checks: -1. **Role-Based Authorization**: Users are assigned roles that determine what they can access. For example, an admin user might have access to all parts of the application, while a regular user might only have access to their profile. -2. **Permission-Based Authorization**: Users are assigned permissions that determine what they can access. For example, a user might have permission to view their profile, but not edit it. +1. **Optimistic Authorization**: +2. **Secure Authorization**: ### Protecting Routes with Middleware From 3782e034af88c2ac2ba1ffb23d0080adc977394d Mon Sep 17 00:00:00 2001 From: Delba de Oliveira Date: Fri, 22 Mar 2024 12:50:07 +0000 Subject: [PATCH 08/22] Work on authorization session (wip) --- .../09-authentication/index.mdx | 288 +++++++++++------- 1 file changed, 177 insertions(+), 111 deletions(-) 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 a17b6e8e14002..025f8021ca177 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 @@ -905,106 +905,216 @@ export default async function handler(req, res) { 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. -There are three main types of authorization checks: +There are two main types of authorization checks: -1. **Optimistic Authorization**: -2. **Secure Authorization**: +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 their permissions. +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 Routes with Middleware +In addition to the above, you should also consider creating a [Data Access Layer](#verifying-sessions-with-a-data-access-layer-dal) to centralize your authorization logic, follow the [Data Transfer Objects (DTO)](#data-transfer-objects-dto) pattern to only return the necessary data, optionally use [Middleware](#optimistic-checks-with-middleware-optional) to perform optimistic checks. -[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. +### Optimistic checks with Middleware (Optional) -Here's how to implement Middleware for authentication in Next.js: +You can 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 routing logic. For example: -1. **Setting Up Middleware:** - - 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. -2. **Defining Protected Routes:** - - Not all routes require authorization. Use the `matcher` option in your Middleware to specify any routes that do not require authorization checks. -3. **Middleware Logic:** - - Write logic to verify if a user is authenticated. Check user roles or permissions for route authorization. -4. **Handling Unauthorized Access:** - - Redirect unauthorized users to a login or error page as appropriate. +```tsx filename="middleware.ts" switcher +import { NextRequest, NextResponse } from 'next/server' +import { decrypt } from '@/app/lib/session' +import { cookies } from 'next/headers' -Example Middleware file: +// 1. Specify protected and public routes +const protectedRoutes = ['/dashboard'] +const publicRoutes = ['/login', '/signup', '/'] -```ts filename="middleware.ts" switcher -import type { NextRequest } from 'next/server' +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) -export function middleware(request: NextRequest) { - const currentUser = request.cookies.get('currentUser')?.value + // 3. Decrypt the session from the cookie + const cookie = cookies().get('session')?.value + const session = await decrypt(cookie) - if (currentUser && !request.nextUrl.pathname.startsWith('/dashboard')) { - return Response.redirect(new URL('/dashboard', request.url)) + // 5. Redirect to /login if the user is not authenticated + if (isProtectedRoute && !session?.userId) { + return NextResponse.redirect(new URL('/login', req.nextUrl)) } - if (!currentUser && !request.nextUrl.pathname.startsWith('/login')) { - return Response.redirect(new URL('/login', request.url)) + // 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="middleware.js" switcher -export function middleware(request) { - const currentUser = request.cookies.get('currentUser')?.value +However, due to [prefetching](/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching), and since Middleware runs on every route, it's important to: + +- Avoid doing heavy computations or database queries in Middleware to prevent performance issues. +- Use Middleware only to read the session from the cookie, and not make any changes to data + +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 closer to your data source, see [Data Access Layer](#verifying-sessions-with-a-data-access-layer) for more information. + +> **Tips**: +> +> - In Middleware, you can also read cookies using `req.cookies.get('session).value`. +> - You can use the `matcher` property in the Middleware to specify which routes Middleware should run on. Although, it's recommended Middleware runs on all routes for authentication. + +### Verifying Sessions with 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, redirect or return the user information needed to make further requests. You can do an **optimistic** or **secure** check, depending on whether you're using [stateless](#stateless-sessions) or [database](#database-sessions) sessions. + +For example, create a separate file for your DAL that includes a `verifySession()` function: + +```tsx filename="app/lib/dal.ts" switcher +import 'server-only' - if (currentUser && !request.nextUrl.pathname.startsWith('/dashboard')) { - return Response.redirect(new URL('/dashboard', request.url)) +import { cookies } from 'next/headers' +import { decrypt } from '@/app/lib/session' + +export async function verifySession() { + const cookie = cookies().get('session')?.value + const session = await decrypt(cookie) + + if (!session?.userId) { + redirect('/login') } - if (!currentUser && !request.nextUrl.pathname.startsWith('/login')) { - return Response.redirect(new URL('/login', request.url)) + return { isAuth: true, userId: session.userId } +} +``` + +You can then invoke the `verifySession()` function in your data requests before performing any operations: + +```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 } +}) +``` + +> **Tip**: +> +> - 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 requests to the database during a render pass. +> - You may wish to consolidate all your data requests in a JavaScript class that runs `verifySession()` before any methods, however, keep in mind that [constructors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/constructor) are synchronous by default and cannot run async code. + +### Data Transfer Objects (DTO) + +When retrieving data, it's recommended you retrieve only the necessary data to 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 object being passed to the client, you should specify what data is ok to be exposed to the client. + +```tsx filename="app/lib/dto.ts" switcher +import 'server-only' +import { getUser } from './dal' + +function canSeeUsername(viewer: User) { + return true } -export const config = { - matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], +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) + + // 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 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. +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 and debug as you scale your application. - +> **Tip**: +> +> - Learn more about security best practices in [Security in Next.js article](/blog/security-nextjs-server-components-actions). -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. +## Server Components -```ts filename="app/page.tsx" switcher -import { redirect } from 'next/navigation' +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 navigation. For example, to display different components based on the user's role: -export default function Page() { - // Logic to determine if a redirect is needed - const accessDenied = true - if (accessDenied) { +```tsx filename="app/dashboard/page.tsx" switcher +import { verifySession } from '@/app/lib/dal' + +export default function Dashboard() { + const session = await verifySession() + const userRole = session?.role + + if (userRole === 'admin') { + return + } else if (userRole === 'user') { + return + } else { redirect('/login') } - - // Define other routes and logic } ``` -```js filename="app/page.jsx" switcher -import { redirect } from 'next/navigation' +```jsx filename="app/dashboard/page.jsx" switcher +import { verifySession } from '@/app/lib/dal' + +export default function Dashboard() { + const session = await verifySession() + const userRole = session.role -export default function Page() { - // Logic to determine if a redirect is needed - const accessDenied = true - if (accessDenied) { + if (userRole === 'admin') { + return + } else if (userRole === 'user') { + return + } else { redirect('/login') } - - // Define other routes and logic } ``` - - -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. +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. -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). +However, due to [Partial Rendering](/docs/app/building-your-application/routing/linking-and-navigating#4-partial-rendering), be cautious when doing checks in shared [Layouts](/docs/app/building-your-application/routing/pages-and-layouts) as these don't re-render on navigation. Instead, you can call the `verifySession() @@ -1079,12 +1189,6 @@ This example demonstrates an API Route with a two-tier security check for authen 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: -- 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. - -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. - #### Protecting Server Actions 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. @@ -1175,59 +1279,15 @@ export async function GET() { 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. -#### Authorization Using Server Components - -[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. - -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. - -**Example:** - -```tsx filename="app/dashboard/page.tsx" switcher -export default function Dashboard() { - const session = await getSession() - const userRole = session?.user?.role // Assuming 'role' is part of the session object - - if (userRole === 'admin') { - return // Component for admin users - } else if (userRole === 'user') { - return // Component for regular users - } else { - return // Component shown for unauthorized access - } -} -``` - -```jsx filename="app/dashboard/page.jsx" switcher -export default function Dashboard() { - const session = await getSession() - const userRole = session?.user?.role // Assuming 'role' is part of the session object - - if (userRole === 'admin') { - return // Component for admin users - } else if (userRole === 'user') { - return // Component for regular users - } else { - return // Component shown for unauthorized access - } -} -``` - -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. - -### Best Practices +## Resources -- **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. +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 in your application: -## Auth Providers +### Auth Providers -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: - -{/* TODO: Change link to authjs.dev when new documentation is ready */} +Here are authentication providers compatible with Next.js, please refer to the quickstart guides below to learn how to configure them in your Next.js application: - [Auth0](https://auth0.com/docs/quickstart/webapp/nextjs/01-login) - [Clerk](https://clerk.com/docs/quickstarts/nextjs) @@ -1236,11 +1296,17 @@ Here are authentication solutions compatible with Next.js, please refer to the q - [NextAuth.js](https://authjs.dev/guides/upgrade-to-v5) - [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 +### 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/) From a73b0dbb19153e6348b52703f8a130e72143cfaf Mon Sep 17 00:00:00 2001 From: Delba de Oliveira Date: Fri, 22 Mar 2024 15:35:45 +0000 Subject: [PATCH 09/22] Add missing JS examples, review --- .../09-authentication/index.mdx | 598 +++++++++++++----- 1 file changed, 453 insertions(+), 145 deletions(-) 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 025f8021ca177..90e3290359445 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 @@ -3,29 +3,29 @@ title: Authentication description: Learn how to implement authentication in your Next.js application. --- -Understanding how to implement authentication is crucial to protect your application's data and ensure a smooth user experience. This page will guide you through the best practices, Next.js and React features, and third-party libraries you can use, so you can choose the right approach for your application. +Understanding auth is crucial protecting your application's data and ensuring a smooth user experience. This page will guide you through how to use Next.js features and third-party libraries to implement authentication, as well as principles and best practices so you can choose the right strategy for your application. -Before starting, it helps to break down the authentication process into three key concepts, or steps: +Before starting, it helps to break down the auth process into three key concepts, or steps: 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 (e.g. logged-in) across requests. +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 provides an overview of the authentication flow in Next.js: +This diagram provides an overview of auth flow in Next.js: {/* TODO: Auth Diagram */} ## Authentication -For educational purposes, the examples in this section will walk you through basic username and password authentication. +For educational purpose, the examples in this section will walk you through basic username and password authentication. -> While you can implement your custom auth solution, for increased security, simplicity and better DX, we recommend using an auth provider. These offer pre-built solutions for authentication, session management, and authorization, as well as additional features like social logins, multi-factor authentication, and role-based access control. You can find a list of Next.js compatible providers in the [Auth Providers](#auth-providers) section. +> While you can implement your custom auth solution, for increased security, simplicity and better DX, we recommend using an authentication provider. These offer pre-built solutions for authentication, session management, and authorization, as well as additional features like social logins, multi-factor authentication, and role-based access control. You can find a list of Next.js compatible providers in the [Authentication Providers](#authentication-providerss) section. ### 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 data, validate form fields, and call your Authentication Provider's API or database. Since Server Actions execute on the server, they provide a secure environment for handling sensitive data. +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 data, validate form fields, and call your Authentication Provider's API or database. Since Server Actions execute on the server, they provide a secure environment for handling authentication logic. Here are the steps to implement a sign-up and/or login form: @@ -82,22 +82,18 @@ export function SignupForm() { ``` ```tsx filename="app/actions/auth.tsx" switcher -export async function signup(formData: FormData) { - // ... -} +export async function signup(formData: FormData) {} ``` ```jsx filename="app/actions/auth.js" switcher -export async function signup(formData) { - // ... -} +export async function signup(formData) {} ``` #### 2. Validate form fields on the server Use the Server Action tos validate the form fields on the 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 schema for your form with appropriate error messages: +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' @@ -131,7 +127,7 @@ export type FormState = | undefined ``` -```ts filename="app/lib/definitions.ts" switcher +```js filename="app/lib/definitions.js" switcher import { z } from 'zod' export const SignupFormSchema = z.object({ @@ -152,9 +148,9 @@ export const SignupFormSchema = z.object({ }) ``` -Then, in your Server Action, after validation, return early if any form fields do not match the criteria. By returning early, you prevent unecessary calls to your authentication provider's API or database: +After validation, `return` early if any form fields do not match the schema to prevent unecessary calls to your authentication provider's API or database: -```tsx filename="app/actions/auth.tsx" switcher +```ts filename="app/actions/auth.ts" switcher import { SignupFormSchema, FormState } from '@/app/lib/definitions' export async function signup(state: FormState, formData: FormData) { @@ -176,6 +172,28 @@ export async function signup(state: FormState, formData: FormData) { } ``` +```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, + } + } + + // TODO: Call provider or db to create user... +} +``` + Back in your ``, you can use React's `useFormState()` hook to conditionally display validation errors to the user: ```tsx filename="app/ui/signup-form.tsx" switcher highlight={6,14,20,26-35} @@ -262,7 +280,7 @@ export function SignupForm() { } ``` -> **Tip:** Remove the `required` attribute from the input fields to allow `useFormState()` to handle validation. `required` uses the browser's native form validation, and will prevent `useFormState()` from showing validation errors. +> **Tip:** Remove the `required` attribute from the input fields to allow `useFormState()` to handle validation. `required` uses the browser's native form validation, and will prevent the form from being submitted. You can also use the `useFormStatus()` hook to handle the pending state on form submission: @@ -271,14 +289,14 @@ You can also use the `useFormStatus()` hook to handle the pending state on form import { useFormStatus, useFormState } from 'react-dom' -// Component +// ... export function SignupButton() { const { pending } = useFormStatus() return ( ) } @@ -289,26 +307,26 @@ export function SignupButton() { import { useFormStatus, useFormState } from 'react-dom' -// Component +// ... export function SignupButton() { const { pending } = useFormStatus() return ( ) } ``` -> **Tip:** `useFormStatus()` should be used in a separate component to avoid re-rendering the entire form when the form status changes. +> **Tip:** `useFormStatus()` should be used in a separate component to avoid re-rendering the entire form when the status changes. See the [React Docs](https://react.dev/docs/hooks-reference#useformstatus) for more information. #### 3. Create user or check user credentials -After validating the form fields, you can create a new user account or check if the user exists by calling your authentication provider's API or database. +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, you should ensure the user's email is unique and passwords are stored securely: +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) { @@ -406,20 +424,21 @@ export async function signup(state, formData) { } ``` -Inversely, on login, you should check if the hashed password matches the user's input. - 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. > **Tips:** > +> - Inversely, on login, you should check if the hashed password matches the user's input. > - 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. Consider debouncing requests with libraries such as [use-debounce](https://www.npmjs.com/package/use-debounce) to manage the frequency of these checks. -> - The example above breaks down the authentication steps for the purpose of teaching, making it verbose. This highlights that implementing your own secure auth solution can quickly become complex and verbose too. Consider using an [Auth Provider](#auth-provider) to simplify the process. +> - The example is verbose since it breaks down the authentication steps for the purpose of teaching. This highlights that implementing your own secure auth solution can quickly become complex and verbose too. Consider using an [Auth Provider](#authentication-providers) to simplify the process. -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. @@ -554,8 +573,8 @@ Session management ensures that an user's authenticated state is preserved acros There are two types of sessions: -1. **Stateless Sessions**: Session data is stored in 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 Sessions**: Session data is stored on a databases, with the user's browser only receiving the encrypted session ID. This method is more secure, but can be more complex and use more server resources. +1. **Stateless**: Session data (or a token) is stored in 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 on a databases, with the user's browser only receiving the encrypted session ID. This method is more secure, but can be more complex and use more server resources. > While you can use either method, or both, we recommend using a third-party library for session management. See [Session Management Libraries](#session-management-libraries) for more information. @@ -566,14 +585,14 @@ There are two types of sessions: To create and manage stateless sessions in your Next.js application, there are a few steps you need to follow: 1. Generate a secret key, which will be used to signed your session, and store the key as an [environment variable](/docs/app/building-your-application/configuring/environment-variables). -2. Encrypt/decrypt session data using a session management library. +2. Write logic to encrypt/decrypt session data (using a session management library). 3. Save the session as 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. In addition to the above, consider adding functionality [update (or extend)](#updating-the-session) the session when the user returns to the application, and [delete](#deleting-the-session) the session when the user logs out. > **Tip:** > -> - If you're using a [Auth Provider](#auth-provider), check if they handle sessions. +> - If you're using a [Auth Provider](#authentication-providers), check if they handle sessions. > - Although the docs mention session, the same principles apply to tokens. #### 1. Generating a secret key @@ -598,7 +617,7 @@ const secretKey = process.env.SESSION_SECRET #### 2. Encrypting and decrypting sessions -Next, you can use your preferred [session management library](#session-management-libraries) to encrypt and decrypt your session. Continuing from the previous example, we'll use [Jose](https://www.npmjs.com/package/jose) (compatible with the Edge Runtime) to demonstrate how to encrypt and decrypt session data: +Next, you can use your preferred [session management library](#session-management-libraries) to encrypt and decrypt your session. 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)) to demonstrate how to encrypt and decrypt session data: ```tsx filename="app/actions/session.ts" switcher import 'server-only' @@ -657,7 +676,7 @@ export async function decrypt(session) { > **Tips**: > > - Use the 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. -> - The payload should 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, or credit card information, or sensitive data like passwords. +> - The payload should 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. #### 3. Setting the cookie (recommended options) @@ -673,7 +692,25 @@ Please refer to [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) Continuing from the previous example, here's an example of how you'd save the session information as cookie: -```tsx filename="app/actions/session.ts" switcher +```ts filename="app/actions/session.ts" switcher +import 'server-only' +import { cookies } from 'next/headers' + +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: '/', + }) +} +``` + +```js filename="app/actions/session.js" switcher import 'server-only' import { cookies } from 'next/headers' @@ -693,7 +730,7 @@ export async function createSession(userId: string) { 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: -```tsx filename="app/actions/auth.ts" switcher +```ts filename="app/actions/auth.ts" switcher import { createSession } from '@/app/actions/session' export async function signup(state: FormState, formData: FormData) { @@ -710,16 +747,33 @@ export async function signup(state: FormState, formData: FormData) { } ``` +```js filename="app/actions/auth.js" switcher +import { createSession } from '@/app/actions/session' + +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 Auth Provider's API + + // Current steps: + // 4. Create user session + await createSession(user.id) + // 5. Redirect user + redirect('/profile') +} +``` + > **Tips**: > -> - For improved security, **cookies should be set on the server** to prevent client-side tampering. +> - **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). #### Updating (or extending) the session You can also extend the session's expiration time. This is useful for keeping the user logged in after they access the appplication again. Ensure you decrypt the session first to make sure it's valid. For example: -```tsx filename="app/actions/session.ts" switcher +```ts filename="app/actions/session.ts" switcher import 'server-only' import { cookies } from 'next/headers' @@ -740,11 +794,32 @@ export async function updateSession() { } ``` +```js filename="app/actions/session.js" switcher +import 'server-only' +import { cookies } from 'next/headers' + +export async function updateSession() { + const session = cookies().get('session').value + if (!session) return + + const parsed = await decrypt(session) + parsed.expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) + + cookies().set('session', session, { + httpOnly: true, + secure: true, + expires: parsed.expires, + sameSite: 'lax', + path: '/', + }) +} +``` + #### Deleting the session To delete the session, you can clear the cookie: -```tsx filename="app/actions/session.ts" switcher +```ts filename="app/actions/session.ts" switcher import 'server-only' import { cookies } from 'next/headers' @@ -753,11 +828,42 @@ export async function deleteSession() { } ``` +```js filename="app/actions/session.js" switcher +import 'server-only' +import { cookies } from 'next/headers' + +export async function deleteSession() { + cookies().delete('session') +} +``` + +Alternatively, you can also use a Server Action to delete the cookie and redirect the user: + +```ts filename="app/actions/auth.ts" switcher +import { cookies } from 'next/headers' + +export async function logout() { + cookies().delete('session') + redirect('/login') +} +``` + +```js filename="app/actions/auth.js" switcher +import { cookies } from 'next/headers' + +export async function logout() { + cookies().delete('session') + redirect('/login') +} +``` + -**Setting a cookie on the server:** +#### 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' @@ -810,7 +916,7 @@ To create and manage database sessions in your Next.js application, you'll need Here's an example of how you can create a new database session, and encrypt the session ID before storing it in a cookie: -```tsx +```ts filename="app/actions/session.ts" switcher import cookies from 'next/headers' export async function createSession(id: number) { @@ -842,12 +948,44 @@ export async function createSession(id: number) { } ``` +```js filename="app/actions/session.js" switcher +import cookies from 'next/headers' + +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 }) + + 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. -> - Database sessions can be used to keep track of user activity, such as 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, or view their active sessions. +> - 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, or view their active sessions. -After implementing session management, you'll need to add authorization logic to allow you control what users can access and do within your application. Continue to the [Authorization](#authorization) section to learn more. +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. @@ -907,14 +1045,14 @@ Once a user is authenticated and a session is created, you can implement authori There are two main types of authorization checks: -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 their permissions. +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. 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. -In addition to the above, you should also consider creating a [Data Access Layer](#verifying-sessions-with-a-data-access-layer-dal) to centralize your authorization logic, follow the [Data Transfer Objects (DTO)](#data-transfer-objects-dto) pattern to only return the necessary data, optionally use [Middleware](#optimistic-checks-with-middleware-optional) to perform optimistic checks. +For both cases, we recommed creating a [Data Access Layer](#verifying-sessions-with-a-data-access-layer-dal) to centralize your authorization logic, use [Data Transfer Objects (DTO)](#data-transfer-objects-dto) only return the necessary data, and optionally use [Middleware](#optimistic-checks-with-middleware-optional) to perform optimistic checks. ### Optimistic checks with Middleware (Optional) -You can 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 routing logic. For example: +You can 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 redirecting logic. For example: ```tsx filename="middleware.ts" switcher import { NextRequest, NextResponse } from 'next/server' @@ -958,19 +1096,64 @@ export const config = { } ``` +```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 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) + + // 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)) + } + + // 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$).*)'], +} +``` + However, due to [prefetching](/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching), and since Middleware runs on every route, it's important to: -- Avoid doing heavy computations or database queries in Middleware to prevent performance issues. -- Use Middleware only to read the session from the cookie, and not make any changes to data +- Avoid doing heavy computations or database queries to prevent performance issues. +- Use Middleware only to read the session from the cookie (optimistic checks). 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 closer to your data source, see [Data Access Layer](#verifying-sessions-with-a-data-access-layer) for more information. > **Tips**: > > - In Middleware, you can also read cookies using `req.cookies.get('session).value`. -> - You can use the `matcher` property in the Middleware to specify which routes Middleware should run on. Although, it's recommended Middleware runs on all routes for authentication. +> - Middleware uses the [Edge Runtime](/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes), check if your session management library is 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. -### Verifying Sessions with a Data Access Layer (DAL) + + +### 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. @@ -996,6 +1179,24 @@ export async function verifySession() { } ``` +```js filename="app/lib/dal.js" switcher +import 'server-only' + +import { cookies } from 'next/headers' +import { decrypt } from '@/app/lib/session' + +export async function verifySession() { + const cookie = cookies().get('session').value + const session = await decrypt(cookie) + + if (!session.userId) { + redirect('/login') + } + + return { isAuth: true, userId: session.userId } +} +``` + You can then invoke the `verifySession()` function in your data requests before performing any operations: ```tsx filename="app/lib/dal.ts" switcher @@ -1026,12 +1227,40 @@ export const getUser = cache(async () => { }) ``` +```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**: > > - 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 requests to the database during a render pass. > - You may wish to consolidate all your data requests in a JavaScript class that runs `verifySession()` before any methods, however, keep in mind that [constructors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/constructor) are synchronous by default and cannot run async code. -### Data Transfer Objects (DTO) +### Using Data Transfer Objects (DTO) When retrieving data, it's recommended you retrieve only the necessary data to 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. @@ -1039,7 +1268,7 @@ However, if you have no control over the data structure, or are working in a tea ```tsx filename="app/lib/dto.ts" switcher import 'server-only' -import { getUser } from './dal' +import { getUser } from '@/app/lib/dal' function canSeeUsername(viewer: User) { return true @@ -1068,13 +1297,44 @@ export async function getProfileDTO(slug: string) { } ``` -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 and debug as you scale your application. +```js filename="app/lib/dto.js" switcher +import 'server-only' +import { getUser } from '@/app/lib/dal' + +function canSeeUsername(viewer) { + return true +} + +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) + + // 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, + } +} +``` + +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 and debug as your application scales. > **Tip**: > > - Learn more about security best practices in [Security in Next.js article](/blog/security-nextjs-server-components-actions). -## Server Components +### 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 navigation. For example, to display different components based on the user's role: @@ -1114,180 +1374,228 @@ export default function Dashboard() { 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. -However, due to [Partial Rendering](/docs/app/building-your-application/routing/linking-and-navigating#4-partial-rendering), be cautious when doing checks in shared [Layouts](/docs/app/building-your-application/routing/pages-and-layouts) as these don't re-render on navigation. Instead, you can call the `verifySession() - - - -### 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. +#### Layouts and auth checks -Here's an example of securing an API Route: +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. -```ts filename="pages/api/route.ts" switcher -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', - }) - return - } +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 auth checks in the data fetching function (`verifySession()` in this case). - // 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 - } +```tsx filename="app/layout.tsx" switcher +export default async function Layout({ + children, +}: { + children: React.ReactNode; +}) { + const user = await getUser(); - // Proceed with the route for authorized users - // ... implementation of the API Route + return ( + // ... + ) } ``` -```js filename="pages/api/route.js" switcher -export default async function handler(req, res) { - const session = await getSession(req) +```jsx filename="app/layout.js" switcher +export default async function Layout({ children }) { + const user = await getUser(); - // 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 - } - - // Proceed with the route for authorized users - // ... implementation of the API Route + return ( + // ... + ) } ``` -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. +This guarantees that wherever `getUser()` is called within your application, the auth check is performed. - +```ts filename="app/lib/dal.ts" switcher +export const getUser = cache(async () => { + const session = await verifySession() + if (!session) return null - + // Get user ID from session and fetch data +}) +``` + +```js filename="app/lib/dal.js" switcher +export const getUser = cache(async () => { + const session = await verifySession() + if (!session) return null -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: + // Get user ID from session and fetch data +}) +``` -#### Protecting Server Actions +#### Server Actions -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. +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. In the example below, we check the user's role before allowing the action to proceed: ```ts filename="app/lib/actions.ts" switcher 'use server' +import { verifySession } from '@/app/lib/dal' -// ... - -export async function serverAction() { - const session = await getSession() +export async function serverAction(formData: FormData) { + const session = await verifySession() const userRole = session?.user?.role - // Check if user is authorized to perform the action + // Return early if user is not authorized to perform the action if (userRole !== 'admin') { - throw new Error('Unauthorized access: User does not have admin privileges.') + return null } // Proceed with the action for authorized users - // ... implementation of the action } ``` ```js filename="app/lib/actions.js" switcher 'use server' - -// ... +import { verifySession } from '@/app/lib/dal' export async function serverAction() { - const session = await getSession() - const userRole = session?.user?.role + const session = await verifySession() + const userRole = session.user.role - // Check if user is authorized to perform the action + // Return early if user is not authorized to perform the action if (userRole !== 'admin') { - throw new Error('Unauthorized access: User does not have admin privileges.') + return null } // Proceed with the action for authorized users - // ... implementation of the action } ``` -#### Protecting Route Handlers +#### Route Handlers -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. +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. Here's an example of securing a Route Handler: ```ts filename="app/api/route.ts" switcher +import { verifySession } from '@/app/lib/dal' + export async function GET() { // User authentication and role verification - const session = await getSession() + const session = await verifySession() // Check if the user is authenticated if (!session) { - return new Response(null, { status: 401 }) // User is not authenticated + // User is not authenticated + return new Response(null, { status: 401 }) } // 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 + // User is authenticated but does not have the right permissions + return new Response(null, { status: 403 }) } - // Data fetching for authorized users + // Continue for authorized users } ``` ```js filename="app/api/route.js" switcher +import { verifySession } from '@/app/lib/dal' + export async function GET() { // User authentication and role verification - const session = await getSession() + const session = await verifySession() // Check if the user is authenticated if (!session) { - return new Response(null, { status: 401 }) // User is not authenticated + // User is not authenticated + return new Response(null, { status: 401 }) } // 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 + // User is authenticated but does not have the right permissions + return new Response(null, { status: 403 }) } - // Data fetching for authorized users + // Continue for authorized users } ``` -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. +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'. + + +### 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 +) { + const session = await getSession(req) + + // 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 + } + + // 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) + + // 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 + } + + // 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. + + + ## Resources -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 in your 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: -### Auth Providers +### Authentication Providers -Here are authentication providers compatible with Next.js, please refer to the quickstart guides below to learn how to configure them in your Next.js application: +Please refer to the quickstart guides below to learn how to configure these Auth Providers in your Next.js application: - [Auth0](https://auth0.com/docs/quickstart/webapp/nextjs/01-login) - [Clerk](https://clerk.com/docs/quickstarts/nextjs) From 12d9fea420ca266d540e8681bc2c579b75622908 Mon Sep 17 00:00:00 2001 From: Delba de Oliveira Date: Fri, 22 Mar 2024 15:46:59 +0000 Subject: [PATCH 10/22] fix broken links --- .../09-authentication/index.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 90e3290359445..0d00533174252 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 @@ -19,7 +19,7 @@ This diagram provides an overview of auth flow in Next.js: For educational purpose, the examples in this section will walk you through basic username and password authentication. -> While you can implement your custom auth solution, for increased security, simplicity and better DX, we recommend using an authentication provider. These offer pre-built solutions for authentication, session management, and authorization, as well as additional features like social logins, multi-factor authentication, and role-based access control. You can find a list of Next.js compatible providers in the [Authentication Providers](#authentication-providerss) section. +> While you can implement your custom auth solution, for increased security, simplicity and better DX, we recommend using an authentication provider. These offer pre-built solutions for authentication, session management, and authorization, as well as additional features like social logins, multi-factor authentication, and role-based access control. You can find a list of Next.js compatible providers in the [Authentication Providers](#authentication-providers) section. @@ -588,7 +588,7 @@ To create and manage stateless sessions in your Next.js application, there are a 2. Write logic to encrypt/decrypt session data (using a session management library). 3. Save the session as 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. -In addition to the above, consider adding functionality [update (or extend)](#updating-the-session) the session when the user returns to the application, and [delete](#deleting-the-session) the session when the user logs out. +In addition to the above, consider adding functionality [update (or extend)](#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. > **Tip:** > @@ -910,7 +910,7 @@ To create and manage database sessions in your Next.js application, you'll need 1. Create a table in your database to store session data (or check if your Auth Provider handles this). 2. Implement functionality to insert, find, 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](#middleware)) +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)). @@ -1048,7 +1048,7 @@ There are two main types of authorization checks: 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. 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. -For both cases, we recommed creating a [Data Access Layer](#verifying-sessions-with-a-data-access-layer-dal) to centralize your authorization logic, use [Data Transfer Objects (DTO)](#data-transfer-objects-dto) only return the necessary data, and optionally use [Middleware](#optimistic-checks-with-middleware-optional) to perform optimistic checks. +For both cases, we recommed creating a [Data Access Layer](#creating-a-data-access-layer-dal) to centralize your authorization logic, use [Data Transfer Objects (DTO)](#using-data-transfer-objects-dto) only return the necessary data, and optionally use [Middleware](#optimistic-checks-with-middleware-optional) to perform optimistic checks. ### Optimistic checks with Middleware (Optional) From b7968e1696172896592482a8e6bacc3833cdef1a Mon Sep 17 00:00:00 2001 From: Delba de Oliveira Date: Fri, 22 Mar 2024 16:58:27 +0000 Subject: [PATCH 11/22] grammar --- .../09-authentication/index.mdx | 81 ++++++++++--------- 1 file changed, 42 insertions(+), 39 deletions(-) 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 0d00533174252..e2e07c04995ff 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 @@ -3,9 +3,9 @@ title: Authentication description: Learn how to implement authentication in your Next.js application. --- -Understanding auth is crucial protecting your application's data and ensuring a smooth user experience. This page will guide you through how to use Next.js features and third-party libraries to implement authentication, as well as principles and best practices so you can choose the right strategy for your application. +Understanding authentication and authorization is crucial protecting your application's data and ensuring a smooth user experience. This page will guide you through how to use Next.js features and third-party libraries to implement auth, as well as principles and best practices so you can choose the right strategy for your application. -Before starting, it helps to break down the auth process into three key concepts, or steps: +Before starting, it helps to break down the process into three key concepts, or steps: 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. @@ -19,13 +19,13 @@ This diagram provides an overview of auth flow in Next.js: For educational purpose, the examples in this section will walk you through basic username and password authentication. -> While you can implement your custom auth solution, for increased security, simplicity and better DX, we recommend using an authentication provider. These offer pre-built solutions for authentication, session management, and authorization, as well as additional features like social logins, multi-factor authentication, and role-based access control. You can find a list of Next.js compatible providers in the [Authentication Providers](#authentication-providers) section. +> While you can implement your custom auth solution, for increased security, simplicity and better DX, you may consider using a third-party authentication provider. They offer in-built 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 of Next.js compatible providers in the [Authentication Providers](#authentication-providers) section. ### 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 data, validate form fields, and call your Authentication Provider's API or database. Since Server Actions execute on the server, they provide a secure environment for handling authentication logic. +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 data, 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 a sign-up and/or login form: @@ -91,7 +91,7 @@ export async function signup(formData) {} #### 2. Validate form fields on the server -Use the Server Action tos validate the form fields on the 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). +Use the Server Action to validate the form fields on the 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: @@ -194,7 +194,7 @@ export async function signup(formData: FormData) { } ``` -Back in your ``, you can use React's `useFormState()` hook to conditionally display validation errors to the user: +Back in your ``, you can use React's `useFormState()` hook to conditionally display validation errors to the user: ```tsx filename="app/ui/signup-form.tsx" switcher highlight={6,14,20,26-35} 'use client' @@ -280,8 +280,6 @@ export function SignupForm() { } ``` -> **Tip:** Remove the `required` attribute from the input fields to allow `useFormState()` to handle validation. `required` uses the browser's native form validation, and will prevent the form from being submitted. - You can also use the `useFormStatus()` hook to handle the pending state on form submission: ```tsx filename="app/ui/signup-form.tsx" switcher @@ -320,7 +318,7 @@ export function SignupButton() { } ``` -> **Tip:** `useFormStatus()` should be used in a separate component to avoid re-rendering the entire form when the status changes. See the [React Docs](https://react.dev/docs/hooks-reference#useformstatus) for more information. +> **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 user or check user credentials @@ -430,7 +428,7 @@ After successfully creating the user account or verifying the user, you can crea > > - Inversely, on login, you should check if the hashed password matches the user's input. > - 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. Consider debouncing requests with libraries such as [use-debounce](https://www.npmjs.com/package/use-debounce) to manage the frequency of these checks. -> - The example is verbose since it breaks down the authentication steps for the purpose of teaching. This highlights that implementing your own secure auth solution can quickly become complex and verbose too. Consider using an [Auth Provider](#authentication-providers) to simplify the process. +> - The example is verbose since it breaks down the authentication steps for the purpose of education. This highlights that implementing your own secure auth solution can quickly become complex and verbose too. Consider using an [Auth Provider](#authentication-providers) to simplify the process. @@ -574,7 +572,7 @@ Session management ensures that an user's authenticated state is preserved acros There are two types of sessions: 1. **Stateless**: Session data (or a token) is stored in 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 on a databases, with the user's browser only receiving the encrypted session ID. This method is more secure, but can be more complex and use more server resources. +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 more complex and use more server resources. > While you can use either method, or both, we recommend using a third-party library for session management. See [Session Management Libraries](#session-management-libraries) for more information. @@ -584,15 +582,15 @@ There are two types of sessions: To create and manage stateless sessions in your Next.js application, there are a few steps you need to follow: -1. Generate a secret key, which will be used to signed your session, and store the key as an [environment variable](/docs/app/building-your-application/configuring/environment-variables). +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 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. 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). -In addition to the above, consider adding functionality [update (or extend)](#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. +In addition to the above, consider adding functionality to [update (or extend)](#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. -> **Tip:** +> **Tips:** > -> - If you're using a [Auth Provider](#authentication-providers), check if they handle sessions. +> - If you're using an [Auth Provider](#authentication-providers), check if they handle sessions. > - Although the docs mention session, the same principles apply to tokens. #### 1. Generating a secret key @@ -611,7 +609,7 @@ SESSION_SECRET=your_secret_key You can then reference this key in your session management logic: -```tsx filename="app/actions/session.ts" switcher +```js filename="app/actions/session.js" const secretKey = process.env.SESSION_SECRET ``` @@ -622,6 +620,7 @@ Next, you can use your preferred [session management library](#session-managemen ```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) @@ -657,7 +656,7 @@ export async function encrypt(payload) { return new SignJWT(payload) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() - .setExpirationTime('7d') // For database sessions + .setExpirationTime('7d') .sign(encodedKey) } @@ -675,7 +674,7 @@ export async function decrypt(session) { > **Tips**: > -> - Use the 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. +> - Use 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. > - The payload should 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. #### 3. Setting the cookie (recommended options) @@ -690,7 +689,7 @@ To store the session in a cookie, use the Next.js [`cookies()`](/docs/app/api-re Please refer to [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) for more information on each of these options. -Continuing from the previous example, here's an example of how you'd save the session information as cookie: +Continuing from the previous example, here's how you'd save the session information as cookie: ```ts filename="app/actions/session.ts" switcher import 'server-only' @@ -819,40 +818,42 @@ export async function updateSession() { To delete the session, you can clear the cookie: -```ts filename="app/actions/session.ts" switcher +```ts filename="app/lib/session.ts" switcher import 'server-only' import { cookies } from 'next/headers' -export async function deleteSession() { +export function deleteSession() { cookies().delete('session') } ``` -```js filename="app/actions/session.js" switcher +```js filename="app/lib/session.js" switcher import 'server-only' import { cookies } from 'next/headers' -export async function deleteSession() { +export function deleteSession() { cookies().delete('session') } ``` -Alternatively, you can also use a Server Action to delete the cookie and redirect the user: +Then you can reuse the `deleteSession()` function in your application, for example within a Server Action: ```ts filename="app/actions/auth.ts" switcher import { cookies } from 'next/headers' +import { deleteSession } from '@/app/actions/session' export async function logout() { - cookies().delete('session') + deleteSession() redirect('/login') } ``` ```js filename="app/actions/auth.js" switcher import { cookies } from 'next/headers' +import { deleteSession } from '@/app/actions/session' export async function logout() { - cookies().delete('session') + deleteSession() redirect('/login') } ``` @@ -906,10 +907,10 @@ export default function handler(req, res) { ### Database Sessions -To create and manage database sessions in your Next.js application, you'll need to follow these steps: +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 Provider handles this). -2. Implement functionality to insert, find, update, and delete sessions. +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)). @@ -918,6 +919,7 @@ Here's an example of how you can create a new database session, and encrypt the ```ts filename="app/actions/session.ts" switcher import cookies from 'next/headers' +import { db } from '@/app/lib/db' export async function createSession(id: number) { const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) @@ -950,6 +952,7 @@ export async function createSession(id: number) { ```js filename="app/actions/session.js" switcher import cookies from 'next/headers' +import { db } from '@/app/lib/db' export async function createSession(id) { const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) @@ -1048,7 +1051,7 @@ There are two main types of authorization checks: 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. 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. -For both cases, we recommed creating a [Data Access Layer](#creating-a-data-access-layer-dal) to centralize your authorization logic, use [Data Transfer Objects (DTO)](#using-data-transfer-objects-dto) only return the necessary data, and optionally use [Middleware](#optimistic-checks-with-middleware-optional) to perform optimistic checks. +For both cases, we recommed creating a [Data Access Layer](#creating-a-data-access-layer-dal) to centralize your authorization logic, use [Data Transfer Objects (DTO)](#using-data-transfer-objects-dto) to only return the necessary data, and optionally use [Middleware](#optimistic-checks-with-middleware-optional) to perform optimistic checks. ### Optimistic checks with Middleware (Optional) @@ -1138,12 +1141,12 @@ export const config = { } ``` -However, due to [prefetching](/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching), and since Middleware runs on every route, it's important to: +However, since Middleware runs on every route, including [prefetched](/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching) routes, it's important to: - Avoid doing heavy computations or database queries to prevent performance issues. - Use Middleware only to read the session from the cookie (optimistic checks). -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 closer to your data source, see [Data Access Layer](#verifying-sessions-with-a-data-access-layer) for more information. +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](#verifying-sessions-with-a-data-access-layer) for more information. > **Tips**: > @@ -1257,14 +1260,14 @@ export const getUser = cache(async () => { > **Tip**: > -> - 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 requests to the database during a render pass. -> - You may wish to consolidate all your data requests in a JavaScript class that runs `verifySession()` before any methods, however, keep in mind that [constructors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/constructor) are synchronous by default and cannot run async code. +> - 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 retrieve only the necessary data to 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. +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 object being passed to the client, you should specify what data is ok to be exposed to the client. +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' @@ -1328,7 +1331,7 @@ export async function getProfileDTO(slug) { } ``` -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 and debug as your application scales. +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. > **Tip**: > @@ -1376,9 +1379,9 @@ In the example, we use the `verifySession()` function from our DAL to check for #### Layouts and auth checks -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. +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 always re-render on navigation. Instead, you should do the checks close to your data source or the component that'll be conditionally rendered. -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 auth checks in the data fetching function (`verifySession()` in this case). +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 the data fetching function (`verifySession()` in this case). ```tsx filename="app/layout.tsx" switcher export default async function Layout({ From 7a26f0909241dacbddb6ecf30b34491d50887a50 Mon Sep 17 00:00:00 2001 From: Delba de Oliveira Date: Fri, 22 Mar 2024 16:59:40 +0000 Subject: [PATCH 12/22] broken link --- .../01-building-your-application/09-authentication/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e2e07c04995ff..08ac7306a1fea 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 @@ -1146,7 +1146,7 @@ However, since Middleware runs on every route, including [prefetched](/docs/app/ - Avoid doing heavy computations or database queries to prevent performance issues. - Use Middleware only to read the session from the cookie (optimistic checks). -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](#verifying-sessions-with-a-data-access-layer) for more information. +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. > **Tips**: > From 00bab030e735ba7daa19c5b176fb80663c83c56d Mon Sep 17 00:00:00 2001 From: Delba de Oliveira Date: Mon, 25 Mar 2024 11:32:06 +0000 Subject: [PATCH 13/22] tweak --- .../09-authentication/index.mdx | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) 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 08ac7306a1fea..5d9511783a937 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 @@ -3,7 +3,7 @@ title: Authentication description: Learn how to implement authentication in your Next.js application. --- -Understanding authentication and authorization is crucial protecting your application's data and ensuring a smooth user experience. This page will guide you through how to use Next.js features and third-party libraries to implement auth, as well as principles and best practices so you can choose the right strategy for your application. +Understanding authentication and authorization is crucial for protecting your application's data and ensuring a smooth user experience. This page will guide you through how to use Next.js features and third-party libraries to implement auth, as well as principles and best practices so you can choose the right strategy for your application. Before starting, it helps to break down the process into three key concepts, or steps: @@ -19,7 +19,7 @@ This diagram provides an overview of auth flow in Next.js: For educational purpose, the examples in this section will walk you through basic username and password authentication. -> While you can implement your custom auth solution, for increased security, simplicity and better DX, you may consider using a third-party authentication provider. They offer in-built 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 of Next.js compatible providers in the [Authentication Providers](#authentication-providers) section. +> While you can implement a custom auth solution, for increased security, simplicity and better DX, you should use a third-party authentication provider. They 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 of Next.js compatible providers in the [Authentication Providers](#authentication-providers) section. @@ -27,7 +27,7 @@ For educational purpose, the examples in this section will walk you through basi 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 data, 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 a sign-up and/or login form: +Here are the steps to implement signup/login functionality: #### 1. Capture user credentials @@ -422,13 +422,14 @@ export async function signup(state, formData) { } ``` +Inversely, on login, you would check if the hashed password matches the user's input. + 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. > **Tips:** > -> - Inversely, on login, you should check if the hashed password matches the user's input. -> - 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. Consider debouncing requests with libraries such as [use-debounce](https://www.npmjs.com/package/use-debounce) to manage the frequency of these checks. -> - The example is verbose since it breaks down the authentication steps for the purpose of education. This highlights that implementing your own secure auth solution can quickly become complex and verbose too. Consider using an [Auth Provider](#authentication-providers) to simplify the process. +> - - The example is verbose since it breaks down the authentication steps for the purpose of education. This highlights that implementing your own secure auth solution can quickly become complex and verbose too. Consider using an [Auth Provider](#authentication-providers) 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. @@ -574,7 +575,7 @@ There are two types of sessions: 1. **Stateless**: Session data (or a token) is stored in 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 more complex and use more server resources. -> While you can use either method, or both, we recommend using a third-party library for session management. See [Session Management Libraries](#session-management-libraries) for more information. +> While you can use either method, or both, you should a library for session management. See [Session Management Libraries](#session-management-libraries) for more information. ### Stateless Sessions @@ -1051,11 +1052,11 @@ There are two main types of authorization checks: 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. 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. -For both cases, we recommed creating a [Data Access Layer](#creating-a-data-access-layer-dal) to centralize your authorization logic, use [Data Transfer Objects (DTO)](#using-data-transfer-objects-dto) to only return the necessary data, and optionally use [Middleware](#optimistic-checks-with-middleware-optional) to perform optimistic checks. +For both cases, we recommed 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, and optionally use [Middleware](#optimistic-checks-with-middleware-optional) to perform optimistic checks. ### Optimistic checks with Middleware (Optional) -You can 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 redirecting logic. For example: +You can 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 redirecting logic. For example: ```tsx filename="middleware.ts" switcher import { NextRequest, NextResponse } from 'next/server' @@ -1151,7 +1152,7 @@ While Middleware can be useful for initial checks, it should not be your only li > **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 session management library is compatible. +> - Middleware uses the [Edge Runtime](/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes), check if your Auth Provider and Seession 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. From 0190014e6959c2fb9a3aa6a4636e798dedd91204 Mon Sep 17 00:00:00 2001 From: Delba de Oliveira <32464864+delbaoliveira@users.noreply.github.com> Date: Thu, 4 Apr 2024 09:49:10 +0100 Subject: [PATCH 14/22] Apply suggestions from code review Co-authored-by: Michael Novotny Co-authored-by: Anthony Shew --- .../09-authentication/index.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 5d9511783a937..66606b05c9c98 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 @@ -45,7 +45,7 @@ export function SignupForm() {
- +
@@ -168,7 +168,7 @@ export async function signup(state: FormState, formData: FormData) { } } - // TODO: Call provider or db to create user... + // Call provider or db to create user... } ``` @@ -190,7 +190,7 @@ export async function signup(formData: FormData) { } } - // TODO: Call provider or db to create user... + // Call provider or db to create user... } ``` @@ -771,7 +771,7 @@ export async function signup(state, formData) { #### Updating (or extending) the session -You can also extend the session's expiration time. This is useful for keeping the user logged in after they access the appplication again. Ensure you decrypt the session first to make sure it's valid. For example: +You can also extend the session's expiration time. This is useful for keeping the user logged in after they access the application again. Ensure you decrypt the session first to make sure it's valid. For example: ```ts filename="app/actions/session.ts" switcher import 'server-only' From 8627e02d72d30bf670faea5545962d33231bbce6 Mon Sep 17 00:00:00 2001 From: Delba de Oliveira <32464864+delbaoliveira@users.noreply.github.com> Date: Thu, 4 Apr 2024 09:50:33 +0100 Subject: [PATCH 15/22] Apply suggestions from code review Co-authored-by: Michael Novotny --- .../01-building-your-application/09-authentication/index.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 66606b05c9c98..3997807d24c06 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 @@ -19,13 +19,13 @@ This diagram provides an overview of auth flow in Next.js: For educational purpose, the examples in this section will walk you through basic username and password authentication. -> While you can implement a custom auth solution, for increased security, simplicity and better DX, you should use a third-party authentication provider. They 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 of Next.js compatible providers in the [Authentication Providers](#authentication-providers) section. +> While you can implement a custom auth solution, for increased security, simplicity, and better DX, you should use a third-party authentication provider. They 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 of Next.js compatible providers in the [Authentication Providers](#authentication-providers) section. ### 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 data, 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. +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: From 71dc50018b4dbada2ab092a254be24df661828fe Mon Sep 17 00:00:00 2001 From: Delba de Oliveira <32464864+delbaoliveira@users.noreply.github.com> Date: Thu, 4 Apr 2024 09:53:32 +0100 Subject: [PATCH 16/22] Update docs/02-app/01-building-your-application/09-authentication/index.mdx Co-authored-by: Michael Novotny --- .../01-building-your-application/09-authentication/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3997807d24c06..40c6432028aec 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 @@ -5,7 +5,7 @@ description: Learn how to implement authentication in your Next.js application. Understanding authentication and authorization is crucial for protecting your application's data and ensuring a smooth user experience. This page will guide you through how to use Next.js features and third-party libraries to implement auth, as well as principles and best practices so you can choose the right strategy for your application. -Before starting, it helps to break down the process into three key concepts, or steps: +Before starting, it helps to break down the process into three key concepts: 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. From da6ec634295668f9dff1a40b6e0f176fd056a0c4 Mon Sep 17 00:00:00 2001 From: Delba de Oliveira Date: Thu, 4 Apr 2024 10:08:45 +0100 Subject: [PATCH 17/22] Apply feedback --- .../09-authentication/index.mdx | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) 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 40c6432028aec..fc9dda2734025 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 @@ -17,15 +17,15 @@ This diagram provides an overview of auth flow in Next.js: ## Authentication -For educational purpose, the examples in this section will walk you through basic username and password authentication. +For educational purposes, the examples in this section will walk you through basic username and password authentication. -> While you can implement a custom auth solution, for increased security, simplicity, and better DX, you should use a third-party authentication provider. They 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 of Next.js compatible providers in the [Authentication Providers](#authentication-providers) section. +> While you can implement a custom auth solution, for increased security, simplicity, and better Developer Experience, you should use a third-party authentication provider. They 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 of Next.js compatible providers in the [Authentication Providers](#authentication-providers) section. ### 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. +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: @@ -69,7 +69,7 @@ export function SignupForm() {
- +
@@ -91,7 +91,7 @@ export async function signup(formData) {} #### 2. Validate form fields on the server -Use the Server Action to validate the form fields on the 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). +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: @@ -196,10 +196,11 @@ export async function signup(formData: FormData) { Back in your ``, you can use React's `useFormState()` hook to conditionally display validation errors to the user: -```tsx filename="app/ui/signup-form.tsx" switcher highlight={6,14,20,26-35} +```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) @@ -238,10 +239,11 @@ export function SignupForm() { } ``` -```jsx filename="app/ui/signup-form.js" switcher highlight={6,14,20,26-35} +```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) @@ -779,15 +781,17 @@ import { cookies } from 'next/headers' export async function updateSession() { const session = cookies().get('session')?.value - if (!session) return + const payload = await decrypt(session) - const parsed = await decrypt(session) - parsed.expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) + if (!session || !payload) { + return null + } + const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) cookies().set('session', session, { httpOnly: true, secure: true, - expires: parsed.expires, + expires: expires, sameSite: 'lax', path: '/', }) @@ -798,17 +802,19 @@ export async function updateSession() { import 'server-only' import { cookies } from 'next/headers' -export async function updateSession() { +eexport async function updateSession() { const session = cookies().get('session').value - if (!session) return + const payload = await decrypt(session) - const parsed = await decrypt(session) - parsed.expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) + if (!session || !payload) { + return null + } + const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) cookies().set('session', session, { httpOnly: true, secure: true, - expires: parsed.expires, + expires: expires, sameSite: 'lax', path: '/', }) From 3abfe1b2c42061a5f62dd1215f3a0f973d977fc0 Mon Sep 17 00:00:00 2001 From: Delba de Oliveira Date: Tue, 16 Apr 2024 13:55:58 +0100 Subject: [PATCH 18/22] Add context providers session --- .../09-authentication/index.mdx | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) 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 e21a9682eb665..53c91144c656f 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 @@ -1528,6 +1528,72 @@ export async function GET() { 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'. +## 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: + +```tsx filename="app/layout.ts" switcher +import { ContextProvider } from 'auth-lib' + +export default function RootLayout({ children }) { + return ( + + + {children} + + + ) +} +``` + +```tsx filename="app/layout.ts" switcher +import { ContextProvider } from 'auth-lib' + +export default function RootLayout({ children }) { + return ( + + + {children} + + + ) +} +``` + +```tsx filename="app/ui/profile.ts switcher +"use client"; + +import { useSession } from "auth-lib"; + +export default function Profile() { + const { userId } = useSession(); + const { data } = useSWR(`/api/user/${userId}`, fetcher) + + return ( + // ... + ); +} +``` + +```jsx filename="app/ui/profile.js switcher +"use client"; + +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) APIs to prevent sensitive session data from being exposed to the client. + From 7027ff7233bc00faa2467df18b4e60b47ba0c51d Mon Sep 17 00:00:00 2001 From: Delba de Oliveira Date: Tue, 16 Apr 2024 13:56:22 +0100 Subject: [PATCH 19/22] Review docs --- .../09-authentication/index.mdx | 148 ++++++++---------- 1 file changed, 68 insertions(+), 80 deletions(-) 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 53c91144c656f..3d1146753ebfb 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 @@ -3,23 +3,21 @@ title: Authentication description: Learn how to implement authentication in your Next.js application. --- -Understanding authentication and authorization is crucial for protecting your application's data and ensuring a smooth user experience. This page will guide you through how to use Next.js features and third-party libraries to implement auth, as well as principles and best practices so you can choose the right strategy for your application. +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. -Before starting, it helps to break down the process into three key concepts: +Before starting, it helps to break down the process into three concepts: 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 provides an overview of auth flow in Next.js: +This diagram shows the auth flow with React and Next.js features: {/* TODO: Auth Diagram */} -## Authentication - -For educational purposes, the examples in this section will walk you through basic username and password authentication. +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 third-party authentication provider. 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 of Next.js examples in the [Vercel Templates Marketplace](https://vercel.com/templates?type=authentication). -> While you can implement a custom auth solution, for increased security, simplicity, and better Developer Experience, you should use a third-party authentication provider. They 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 of Next.js compatible providers in the [Authentication Providers](#authentication-providers) section. +## Authentication @@ -148,7 +146,7 @@ export const SignupFormSchema = z.object({ }) ``` -After validation, `return` early if any form fields do not match the schema to prevent unecessary calls to your authentication provider's API or database: +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' @@ -168,7 +166,7 @@ export async function signup(state: FormState, formData: FormData) { } } - // Call provider or db to create user... + // Call the provider or db to create a user... } ``` @@ -190,11 +188,11 @@ export async function signup(formData: FormData) { } } - // Call provider or db to create user... + // Call the provider or db to create a user... } ``` -Back in your ``, you can use React's `useFormState()` hook to conditionally display validation errors to the 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' @@ -322,7 +320,7 @@ export function SignupButton() { > **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 user or check user credentials +#### 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. @@ -351,7 +349,7 @@ export async function signup(state: FormState, formData: FormData) { // 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 Provider's API + // 3. Insert the user into the database or call an Auth Library's API const data = await db .insert(users) .values({ @@ -399,7 +397,7 @@ export async function signup(state, formData) { // 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 Provider's API + // 3. Insert the user into the database or call an Library API const data = await db .insert(users) .values({ @@ -424,13 +422,11 @@ export async function signup(state, formData) { } ``` -Inversely, on login, you would check if the hashed password matches the user's input. - 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. > **Tips:** > -> - - The example is verbose since it breaks down the authentication steps for the purpose of education. This highlights that implementing your own secure auth solution can quickly become complex and verbose too. Consider using an [Auth Provider](#authentication-providers) to simplify the process. +> - 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](https://vercel.com/templates?type=authentication) 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. @@ -570,14 +566,14 @@ export default async function handler(req, res) { ## Session Management -Session management ensures that an user's authenticated state is preserved across requests, and sometimes, devices. It involves creating, storing, updating, and deleting sessions. +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 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 more complex and use more server resources. +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, you should a library for session management. See [Session Management Libraries](#session-management-libraries) for more information. +> 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 @@ -586,25 +582,24 @@ There are two types of sessions: To create and manage stateless sessions in your Next.js application, there are a few steps you need to follow: 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). +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). -In addition to the above, consider adding functionality to [update (or extend)](#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. +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. > **Tips:** > -> - If you're using an [Auth Provider](#authentication-providers), check if they handle sessions. -> - Although the docs mention session, the same principles apply to tokens. +> - If you're using an [auth library](https://vercel.com/templates?type=authentication), check if they handle sessions. #### 1. Generating a secret key -You'll need a secret key to sign the session data. There are a few ways you can generate this key. For example, you may choose to use the `openssl` command in your terminal: +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 ``` -This command generates a 32-character random string that you can use as your secret key. Store this key in your environment variables file: +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): ```bash filename=".env" SESSION_SECRET=your_secret_key @@ -618,7 +613,7 @@ const secretKey = process.env.SESSION_SECRET #### 2. Encrypting and decrypting sessions -Next, you can use your preferred [session management library](#session-management-libraries) to encrypt and decrypt your session. 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)) to demonstrate how to encrypt and decrypt session data: +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' @@ -675,10 +670,11 @@ export async function decrypt(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. + > **Tips**: > -> - Use 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. -> - The payload should 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. +> - 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. #### 3. Setting the cookie (recommended options) @@ -692,7 +688,7 @@ To store the session in a cookie, use the Next.js [`cookies()`](/docs/app/api-re Please refer to [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) for more information on each of these options. -Continuing from the previous example, here's how you'd save the session information as cookie: +Continuing from the previous example, here's how you'd save the encrypted session as a cookie: ```ts filename="app/actions/session.ts" switcher import 'server-only' @@ -733,13 +729,13 @@ export async function createSession(userId: string) { 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: ```ts filename="app/actions/auth.ts" switcher -import { createSession } from '@/app/actions/session' +import { createSession } from '@/app/lib/session' 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 Auth Provider's API + // 3. Insert the user into the database or call an Library API // Current steps: // 4. Create user session @@ -750,13 +746,13 @@ export async function signup(state: FormState, formData: FormData) { ``` ```js filename="app/actions/auth.js" switcher -import { createSession } from '@/app/actions/session' +import { createSession } from '@/app/lib/session' 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 Auth Provider's API + // 3. Insert the user into the database or call an Library API // Current steps: // 4. Create user session @@ -771,9 +767,9 @@ export async function signup(state, formData) { > - **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). -#### Updating (or extending) the session +#### Updating (or refreshing) sessions -You can also extend the session's expiration time. This is useful for keeping the user logged in after they access the application again. Ensure you decrypt the session first to make sure it's valid. For example: +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: ```ts filename="app/actions/session.ts" switcher import 'server-only' @@ -821,9 +817,13 @@ eexport async function updateSession() { } ``` +> **Tips:** +> +> - If using an authentication library, check if they support refresh tokens. + #### Deleting the session -To delete the session, you can clear the cookie: +To delete the session, you can delete the cookie: ```ts filename="app/lib/session.ts" switcher import 'server-only' @@ -843,7 +843,7 @@ export function deleteSession() { } ``` -Then you can reuse the `deleteSession()` function in your application, for example within a Server Action: +Then you can reuse the `deleteSession()` function in your application, for example, on logout: ```ts filename="app/actions/auth.ts" switcher import { cookies } from 'next/headers' @@ -916,7 +916,7 @@ export default function handler(req, res) { 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 Provider handles this). +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)). @@ -993,7 +993,7 @@ export async function createSession(id) { > **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, or view their active sessions. +> - 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 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. @@ -1055,14 +1055,23 @@ Once a user is authenticated and a session is created, you can implement authori There are two main types of authorization checks: -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. +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. -For both cases, we recommed 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, and optionally use [Middleware](#optimistic-checks-with-middleware-optional) to perform optimistic checks. +For both cases, we recommend: + +- 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. ### Optimistic checks with Middleware (Optional) -You can 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 redirecting logic. For example: +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). + +For example: ```tsx filename="middleware.ts" switcher import { NextRequest, NextResponse } from 'next/server' @@ -1148,17 +1157,14 @@ export const config = { } ``` -However, since Middleware runs on every route, including [prefetched](/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching) routes, it's important to: - -- Avoid doing heavy computations or database queries to prevent performance issues. -- Use Middleware only to read the session from the cookie (optimistic checks). +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. 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. > **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 Provider and Seession management library are compatible. +> - 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. @@ -1167,9 +1173,9 @@ While Middleware can be useful for initial checks, it should not be your only li 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, redirect or return the user information needed to make further requests. You can do an **optimistic** or **secure** check, depending on whether you're using [stateless](#stateless-sessions) or [database](#database-sessions) sessions. +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: +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' @@ -1177,7 +1183,7 @@ import 'server-only' import { cookies } from 'next/headers' import { decrypt } from '@/app/lib/session' -export async function verifySession() { +export const verifySession = cache(async () => { const cookie = cookies().get('session')?.value const session = await decrypt(cookie) @@ -1186,7 +1192,7 @@ export async function verifySession() { } return { isAuth: true, userId: session.userId } -} +}) ``` ```js filename="app/lib/dal.js" switcher @@ -1195,7 +1201,7 @@ import 'server-only' import { cookies } from 'next/headers' import { decrypt } from '@/app/lib/session' -export async function verifySession() { +export const verifySession = cache(async () => { const cookie = cookies().get('session').value const session = await decrypt(cookie) @@ -1204,10 +1210,10 @@ export async function verifySession() { } return { isAuth: true, userId: session.userId } -} +}) ``` -You can then invoke the `verifySession()` function in your data requests before performing any operations: +You can then invoke the `verifySession()` function in your data requests and Server Actions before performing any operations: ```tsx filename="app/lib/dal.ts" switcher // ... @@ -1267,6 +1273,7 @@ export const getUser = cache(async () => { > **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. @@ -1340,13 +1347,11 @@ export async function getProfileDTO(slug) { 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. -> **Tip**: -> -> - Learn more about security best practices in [Security in Next.js article](/blog/security-nextjs-server-components-actions). +> **Good to know**: Learn more about security best practices in [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 navigation. For example, to display different components based on the user's role: +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 import { verifySession } from '@/app/lib/dal' @@ -1370,7 +1375,7 @@ import { verifySession } from '@/app/lib/dal' export default function Dashboard() { const session = await verifySession() - const userRole = session.role + const userRole = session.role // Assuming 'role' is part of the session object if (userRole === 'admin') { return @@ -1386,9 +1391,9 @@ In the example, we use the `verifySession()` function from our DAL to check for #### Layouts and auth checks -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 always re-render on navigation. Instead, you should do the checks close to your data source or the component that'll be conditionally rendered. +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. -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 the data fetching function (`verifySession()` in this case). +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. ```tsx filename="app/layout.tsx" switcher export default async function Layout({ @@ -1669,23 +1674,6 @@ This example demonstrates an API Route with a two-tier security check for authen 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: -### Authentication Providers - -Please refer to the quickstart guides below to learn how to configure these Auth Providers in your Next.js application: - -- [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/guides/upgrade-to-v5) -- [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: From dca3120aa6a7c483906c8ec1bdd2cdd5af9b0a48 Mon Sep 17 00:00:00 2001 From: Delba de Oliveira Date: Tue, 16 Apr 2024 14:00:06 +0100 Subject: [PATCH 20/22] Fix broken links --- .../01-building-your-application/09-authentication/index.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 1d28c94dcb908..484d0e47211f1 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 @@ -585,7 +585,7 @@ To create and manage stateless sessions in your Next.js application, there are a 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). -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. +In addition to the above, consider adding functionality to [update (or refresh)](#updating-or-refreshing-sessions) the session when the user returns to the application, and [delete](#deleting-the-session) the session when the user logs out. > **Tips:** > @@ -613,7 +613,7 @@ const secretKey = process.env.SESSION_SECRET #### 2. Encrypting and decrypting sessions -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)): +Next, you can use your preferred session management library 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' From 5d3cfdb423590dbec47d2c0dab2a43237dcb497a Mon Sep 17 00:00:00 2001 From: Delba de Oliveira Date: Tue, 16 Apr 2024 17:50:46 +0100 Subject: [PATCH 21/22] Add diagram --- .../09-authentication/index.mdx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 484d0e47211f1..7d3916ea1272d 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 @@ -13,7 +13,13 @@ Before starting, it helps to break down the process into three concepts: This diagram shows the auth flow with React and Next.js features: -{/* TODO: Auth Diagram */} +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 third-party authentication provider. 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 of Next.js examples in the [Vercel Templates Marketplace](https://vercel.com/templates?type=authentication). From d7658aeb0596cb43abd389dc10f2fec4da94fde1 Mon Sep 17 00:00:00 2001 From: Delba de Oliveira Date: Tue, 16 Apr 2024 17:51:06 +0100 Subject: [PATCH 22/22] Bring back guides for auth libraries --- .../09-authentication/index.mdx | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) 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 7d3916ea1272d..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 @@ -21,7 +21,7 @@ This diagram shows the auth flow with React and Next.js features: height="1383" /> -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 third-party authentication provider. 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 of Next.js examples in the [Vercel Templates Marketplace](https://vercel.com/templates?type=authentication). +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 @@ -432,7 +432,7 @@ After successfully creating the user account or verifying the user, you can crea > **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](https://vercel.com/templates?type=authentication) to simplify the process. +> - 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. @@ -591,11 +591,11 @@ To create and manage stateless sessions in your Next.js application, there are a 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). -In addition to the above, consider adding functionality to [update (or refresh)](#updating-or-refreshing-sessions) the session when the user returns to the application, and [delete](#deleting-the-session) the session when the user logs out. +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. > **Tips:** > -> - If you're using an [auth library](https://vercel.com/templates?type=authentication), check if they handle sessions. +> - If you're using an [auth library](#auth-libraries), check if they handle sessions. #### 1. Generating a secret key @@ -619,7 +619,7 @@ const secretKey = process.env.SESSION_SECRET #### 2. Encrypting and decrypting sessions -Next, you can use your preferred session management library 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)): +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' @@ -1353,7 +1353,10 @@ export async function getProfileDTO(slug) { 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**: Learn more about security best practices in [Security in Next.js article](/blog/security-nextjs-server-components-actions). +> **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 @@ -1445,6 +1448,10 @@ export const getUser = cache(async () => { }) ``` +> **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. + #### 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. @@ -1603,7 +1610,7 @@ export default function Profile() { } ``` -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) APIs to prevent sensitive session data from being exposed to the client. +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. @@ -1676,6 +1683,25 @@ This example demonstrates an API Route with a two-tier security check for authen +## Resources + +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: + +### 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/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: