diff --git a/README.md b/README.md index 8a5934c..a8f9652 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![CircleCI](https://img.shields.io/circleci/build/github/auth0-samples/auth0-nextjs-samples?style=flat-square)](https://circleci.com/gh/auth0-samples/auth0-nextjs-samples) [![License](https://img.shields.io/:license-mit-blue.svg?style=flat)](https://opensource.org/licenses/MIT) -This is the sample code for the [Auth0 Next.js Quickstart](https://auth0.com/docs/quickstart/webapp/nextjs) using [nextjs-auth0](https://github.com/auth0/nextjs-auth0). +This is the sample code for the [Auth0 Next.js Quickstart](https://auth0.com/docs/quickstart/webapp/nextjs) using [nextjs-auth0 v4](https://github.com/auth0/nextjs-auth0). Please check out [the sample application](./Sample-01) for an example of how to integrate the Auth0 Next.js SDK into your Next.js applications. diff --git a/Sample-01/.babelrc b/Sample-01/.babelrc index 1ff94f7..9c0bf12 100644 --- a/Sample-01/.babelrc +++ b/Sample-01/.babelrc @@ -1,3 +1,8 @@ { - "presets": ["next/babel"] -} + "presets": [ + "@babel/preset-env", + ["@babel/preset-react", { "runtime": "automatic" }], + "@babel/preset-typescript" + ], + "plugins": [] +} \ No newline at end of file diff --git a/Sample-01/.env.local.example b/Sample-01/.env.local.example index 4ad54e3..57f5f69 100644 --- a/Sample-01/.env.local.example +++ b/Sample-01/.env.local.example @@ -1,7 +1,5 @@ AUTH0_SECRET=replace-with-your-own-secret-generated-with-openssl -AUTH0_BASE_URL=http://localhost:3000 -AUTH0_ISSUER_BASE_URL='https://{DOMAIN}' -AUTH0_CLIENT_ID='{CLIENT_ID}' -AUTH0_CLIENT_SECRET='{CLIENT_SECRET}' -AUTH0_AUDIENCE= -AUTH0_SCOPE='openid profile' +APP_BASE_URL=http://localhost:3000 +AUTH0_DOMAIN={DOMAIN} +AUTH0_CLIENT_ID={CLIENT_ID} +AUTH0_CLIENT_SECRET={CLIENT_SECRET} \ No newline at end of file diff --git a/Sample-01/README.md b/Sample-01/README.md index 030db33..395bdb8 100644 --- a/Sample-01/README.md +++ b/Sample-01/README.md @@ -1,14 +1,14 @@ # Auth0 Next.js SDK Sample Application -This sample demonstrates the integration of [Auth0 Next.js SDK](https://github.com/auth0/nextjs-auth0) into a Next.js application created using [create-next-app](https://nextjs.org/docs/api-reference/create-next-app). The sample is a companion to the [Auth0 Next.js SDK Quickstart](https://auth0.com/docs/quickstart/webapp/nextjs). +This sample demonstrates the integration of [Auth0 Next.js SDK v4](https://github.com/auth0/nextjs-auth0) into a Next.js application created using [create-next-app](https://nextjs.org/docs/api-reference/create-next-app). The sample is a companion to the [Auth0 Next.js SDK Quickstart](https://auth0.com/docs/quickstart/webapp/nextjs). This sample demonstrates the following use cases: -- [Login](https://github.com/auth0-samples/auth0-nextjs-samples/blob/main/Sample-01/components/NavBar.jsx#L61-L67) -- [Logout](https://github.com/auth0-samples/auth0-nextjs-samples/blob/main/Sample-01/components/NavBar.jsx#L93-L95) -- [Showing the user profile](https://github.com/auth0-samples/auth0-nextjs-samples/blob/main/Sample-01/pages/profile.jsx) -- [Protecting client-side rendered pages](https://github.com/auth0-samples/auth0-nextjs-samples/blob/main/Sample-01/pages/profile.jsx#L43-L46) -- [Calling APIs](https://github.com/auth0-samples/auth0-nextjs-samples/blob/main/Sample-01/pages/external.jsx) +- Login +- Logout +- Showing the user profile +- Protecting routes using middleware +- Calling APIs with access tokens ## Project setup @@ -22,37 +22,329 @@ npm install ### Create an API -For the **External API** page to work, you will need to [create an API](https://auth0.com/docs/authorization/apis) using the [management dashboard](https://manage.auth0.com/#/apis). This will give you an API Identifier that you can use in the `AUTH0_AUDIENCE` environment variable below. Then you will need to [add a permission](https://auth0.com/docs/get-started/dashboard/add-api-permissions) named `read:shows` to your API. To get your app to ask for that permission, include it in the value of the `AUTH0_SCOPE` environment variable. +For the **External API** page to work, you will need to [create an API](https://auth0.com/docs/authorization/apis) using the [management dashboard](https://manage.auth0.com/#/apis). This will give you an API Identifier that you can use in the `AUTH0_AUDIENCE` environment variable below. Then you will need to [add a permission](https://auth0.com/docs/get-started/dashboard/add-api-permissions) named `read:shows` to your API. -If you do not wish to use an API or observe the API call working, you should not specify the `AUTH0_AUDIENCE` and `AUTH0_SCOPE` values in the next steps. +If you do not wish to use an API or observe the API call working, you should not specify the `AUTH0_AUDIENCE` value in the next steps. ### Configure credentials The project needs to be configured with your Auth0 Domain, Client ID and Client Secret for the authentication flow to work. -To do this, first copy `.env.local.example` into a new file in the same folder called `.env.local`, and replace the values with your own Auth0 application credentials (see more info about [loading environmental variables in Next.js](https://nextjs.org/docs/basic-features/environment-variables)): +To do this, first copy `.env.local.example` into a new file in the same folder called `.env.local`, and replace the values with your own Auth0 application credentials: ```sh # A long secret value used to encrypt the session cookie AUTH0_SECRET='LONG_RANDOM_VALUE' -# The base url of your application -AUTH0_BASE_URL='http://localhost:3000' -# The url of your Auth0 tenant domain -AUTH0_ISSUER_BASE_URL='https://YOUR_AUTH0_DOMAIN.auth0.com' # Your Auth0 application's Client ID AUTH0_CLIENT_ID='YOUR_AUTH0_CLIENT_ID' # Your Auth0 application's Client Secret AUTH0_CLIENT_SECRET='YOUR_AUTH0_CLIENT_SECRET' -# Your Auth0 API's Identifier -# OMIT if you do not want to use the API part of the sample -AUTH0_AUDIENCE='YOUR_AUTH0_API_IDENTIFIER' -# The permissions your app is asking for -# OMIT if you do not want to use the API part of the sample -AUTH0_SCOPE='openid profile email read:shows' +# Your Auth0 tenant domain +AUTH0_DOMAIN='YOUR_AUTH0_DOMAIN.auth0.com' +# The base url of your application +APP_BASE_URL='http://localhost:3000' ``` **Note**: Make sure you replace `AUTH0_SECRET` with your own secret (you can generate a suitable string using `openssl rand -hex 32` on the command line). +## Step-by-Step Implementation Guide + +This guide will walk you through integrating Auth0 with Next.js using the Auth0 Next.js SDK v4. + +### Step 1: Create a Next.js App + +If you don't have an existing Next.js application, create one using: + +```bash +npx create-next-app@latest my-auth0-app +cd my-auth0-app +``` + +### Step 2: Install the Auth0 SDK + +Install the Auth0 Next.js SDK v4: + +```bash +npm install @auth0/nextjs-auth0@4.2.0 +``` + +### Step 3: Set Up Auth0 Configuration + +1. **Set up your Auth0 account**: + - Sign up for an Auth0 account at [https://auth0.com](https://auth0.com) if you don't have one + - Create a new application in the Auth0 Dashboard + - Choose "Regular Web Application" as the application type + +2. **Configure Auth0 application settings**: + - Set the "Allowed Callback URLs" to `http://localhost:3000/auth/callback` + - Set the "Allowed Logout URLs" to `http://localhost:3000` + - Set the "Allowed Web Origins" to `http://localhost:3000` + +3. **Create the environment variables file**: + - Create a `.env.local` file in your project root with the following variables: + + + ```sh + # A long secret value used to encrypt the session cookie + AUTH0_SECRET='LONG_RANDOM_VALUE' + # The base url of your application + APP_BASE_URL='http://localhost:3000' + # Your Auth0 application's Client ID + AUTH0_CLIENT_ID='YOUR_AUTH0_CLIENT_ID' + # Your Auth0 application's Client Secret + AUTH0_CLIENT_SECRET='YOUR_AUTH0_CLIENT_SECRET' + ``` + +### Step 4: Initialize the Auth0 Client + +Create a file at `lib/auth0.ts` with the following content: + +```typescript +import { Auth0Client } from "@auth0/nextjs-auth0/server"; + +export const auth0 = new Auth0Client({ + // Options are loaded from environment variables by default + authorizationParameters: { + scope: "openid profile email", + } +}); +``` + +If you need to call an API, adjust the scope to include the required permissions: + +```typescript +authorizationParameters: { + scope: "openid profile email read:shows", +} +``` + +### Step 5: Set Up Middleware for Route Protection + +Create a file named `middleware.ts` in your project root: + +```typescript +import type { NextRequest } from "next/server"; +import { auth0 } from "./lib/auth0"; + +export async function middleware(request: NextRequest) { + return await auth0.middleware(request); +} + +export const config = { + matcher: [ + "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)", + ], +}; +``` + +This middleware will handle authentication for all routes except static assets. + +### Step 6: Create a Profile Page + +Create a profile page to display user information at `app/profile/page.tsx`: + +```typescript +import { auth0 } from "../../lib/auth0"; +import { redirect } from "next/navigation"; + +export default async function ProfilePage() { + const session = await auth0.getSession(); + + // Redirect to login if not authenticated + if (!session?.user) { + return redirect("/auth/login"); + } + + return ( +
+

Profile

+

{session.user.name}

+

{session.user.email}

+ Profile + +
{JSON.stringify(session.user, null, 2)}
+
+ ); +} +``` + +### Step 7: Create Navigation Component + +Create a navigation component with login/logout buttons at `components/NavBar.tsx`: + +```typescript +import Link from "next/link"; +import { auth0 } from "../lib/auth0"; + +export default async function NavBar() { + const session = await auth0.getSession(); + const isAuthenticated = !!session?.user; + + return ( + + ); +} +``` + +### Step 8: Integrate API Access (Optional) + +If you want to access an external API with the user's access token, create an API route: + +```typescript +// app/api/shows/route.js +import { auth0 } from '../../../lib/auth0'; +import { NextResponse } from 'next/server'; + +export async function GET(req) { + // Check if user is authenticated + const session = await auth0.getSession(); + if (!session) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + try { + // Get access token + const { accessToken } = await auth0.getAccessToken(); + + const apiPort = process.env.API_PORT || 3001; + const response = await fetch(`http://localhost:${apiPort}/api/shows`, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + const shows = await response.json(); + + return NextResponse.json(shows); + } catch (error) { + console.error('Auth0 or API error:', error); + return NextResponse.json({ error: error.message }, { status: error.status || 500 }); + } +} +``` + +### Step 9: Update Root Layout + +Add the NavBar to your root layout at `app/layout.tsx`: + +```typescript +import { Inter } from 'next/font/google'; +import NavBar from '../components/NavBar'; + +const inter = Inter({ subsets: ['latin'] }); + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + +
{children}
+ + + ); +} +``` + +### Step 11: Start your Application + +Start your Next.js application: + +```bash +npm run dev +``` + +Visit http://localhost:3000 and test the authentication flow. + +## Key Changes in v4 + +This sample has been updated to use Auth0 NextJS SDK v4, which includes several important changes: + +1. **New Auth0Client Class**: We now use the `Auth0Client` from `@auth0/nextjs-auth0/server` which provides a simpler API. + +2. **Middleware-based Protection**: Route protection now uses Next.js middleware instead of higher-order components or hooks for server-side protection. + +3. **App Router Support**: The sample now works with Next.js App Router, with API routes implemented as route handlers. + +5. **Environment Variables**: The configuration has been simplified with the required environment variables loaded automatically. + +## Implementation Details + +### Auth0 Client Initialization + +```typescript +// lib/auth0.ts +import { Auth0Client } from "@auth0/nextjs-auth0/server"; + +export const auth0 = new Auth0Client({ + // Options are loaded from environment variables by default + authorizationParameters: { + scope: "openid profile email offline_access read:shows", + } +}); +``` + +### Middleware for Route Protection + +```typescript +// middleware.ts +import type { NextRequest } from "next/server" +import { auth0 } from "./lib/auth0" + +export async function middleware(request: NextRequest) { + return await auth0.middleware(request) +} + +export const config = { + matcher: [ + "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)", + ], +} +``` + +### API Route Implementation + +```javascript +// app/api/shows/route.js +import { auth0 } from '../../../lib/auth0'; +import { NextResponse } from 'next/server'; + +export async function GET(req) { + const session = await auth0.getSession(); + if (!session) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + try { + const { accessToken } = await auth0.getAccessToken(); + // Use the access token to call your API + // ... + } catch (error) { + // Error handling + } +} +``` + ## Run the sample ### Compile and hot-reload for development diff --git a/Sample-01/api-server.js b/Sample-01/api-server.js index 822a932..d768674 100644 --- a/Sample-01/api-server.js +++ b/Sample-01/api-server.js @@ -9,32 +9,32 @@ const jwksRsa = require('jwks-rsa'); const app = express(); const port = process.env.API_PORT || 3001; -const baseUrl = process.env.AUTH0_BASE_URL; -const issuerBaseUrl = process.env.AUTH0_ISSUER_BASE_URL; -const audience = process.env.AUTH0_AUDIENCE; +const baseUrl = process.env.APP_BASE_URL; +const domain = process.env.AUTH0_DOMAIN; -if (!baseUrl || !issuerBaseUrl) { +if (!baseUrl) { throw new Error('Please make sure that the file .env.local is in place and populated'); } -if (!audience) { - console.log('AUTH0_AUDIENCE not set in .env.local. Shutting down API server.'); - process.exit(1); -} - app.use(morgan('dev')); app.use(helmet()); app.use(cors({ origin: baseUrl })); +app.use((req, res, next) => { + console.log('Authorization header:', req.headers.authorization); + next(); +}); + + const checkJwt = jwt({ secret: jwksRsa.expressJwtSecret({ cache: true, rateLimit: true, jwksRequestsPerMinute: 5, - jwksUri: `${issuerBaseUrl}/.well-known/jwks.json` + jwksUri: `https://${domain}/.well-known/jwks.json` }), - audience: audience, - issuer: `${issuerBaseUrl}/`, + audience: `https://${domain}/api/v2/`, + issuer: `https://${domain}/`, algorithms: ['RS256'] }); diff --git a/Sample-01/app/api/auth/[auth0]/route.js b/Sample-01/app/api/auth/[auth0]/route.js deleted file mode 100644 index 1d2e4e4..0000000 --- a/Sample-01/app/api/auth/[auth0]/route.js +++ /dev/null @@ -1,3 +0,0 @@ -import { handleAuth } from '@auth0/nextjs-auth0'; - -export const GET = handleAuth(); diff --git a/Sample-01/app/api/shows/route.js b/Sample-01/app/api/shows/route.js index 6fe9042..4f484c5 100644 --- a/Sample-01/app/api/shows/route.js +++ b/Sample-01/app/api/shows/route.js @@ -1,22 +1,45 @@ -import { getAccessToken, withApiAuthRequired } from '@auth0/nextjs-auth0'; +import { auth0 } from '../../../lib/auth0'; import { NextResponse } from 'next/server'; -export const GET = withApiAuthRequired(async function shows(req) { +export async function GET(req) { try { - const res = new NextResponse(); - const { accessToken } = await getAccessToken(req, res, { - scopes: ['read:shows'] - }); + // Check if user is authenticated + const session = await auth0.getSession(); + if (!session) { + return NextResponse.json( + { error: 'Not authenticated' }, + { status: 401 } + ); + } + + const accessToken = await auth0.getAccessToken(); + // console.log('Access token:', accessToken); + const apiPort = process.env.API_PORT || 3001; - const response = await fetch(`http://localhost:${apiPort}/api/shows`, { + const apiUrl = `http://localhost:${apiPort}/api/shows`; + + const response = await fetch(apiUrl, { headers: { - Authorization: `Bearer ${accessToken}` + 'Authorization': `Bearer ${accessToken.token}` } }); + + // Check if response is unsuccessful + if (!response.ok) { + const errorText = await response.text(); + console.error('API response error:', response.status, errorText); + return NextResponse.json( + { error: `API error: ${response.status}` }, + { status: response.status } + ); + } + + const shows = await response.json(); - return NextResponse.json(shows, res); + return NextResponse.json(shows); } catch (error) { + console.error('Auth0 or API error:', error); return NextResponse.json({ error: error.message }, { status: error.status || 500 }); } -}); +} diff --git a/Sample-01/app/csr/page.jsx b/Sample-01/app/csr/page.jsx index 9e232d2..bf8632b 100644 --- a/Sample-01/app/csr/page.jsx +++ b/Sample-01/app/csr/page.jsx @@ -1,29 +1,32 @@ 'use client'; import React from 'react'; -import { withPageAuthRequired } from '@auth0/nextjs-auth0/client'; +import useAuthGuard from '../../hooks/useAuthGuard'; + +export default function CSRPage() { + const { isAuthenticated, guardComponent } = useAuthGuard(); + + // If not authenticated or loading, show guard component + if (!isAuthenticated) return guardComponent; -export default withPageAuthRequired(function CSRPage() { return ( - <> -
-

Client-side Rendered Page

-
-

- You can protect a client-side rendered page by wrapping it with withPageAuthRequired. Only - logged in users will be able to access it. If the user is logged out, they will be redirected to the login - page instead. -

-

- Use the useUser hook to access the user profile from protected client-side rendered pages. The{' '} - useUser hook relies on the UserProvider Context Provider, so you need to wrap your - custom App Component with it. -

-

- You can also fetch the user profile by calling the /api/auth/me API route. -

-
+
+

Client-side Rendered Page

+
+

+ You can protect a client-side rendered page by wrapping it with withPageAuthRequired. Only + logged in users will be able to access it. If the user is logged out, they will be redirected to the login + page instead. +

+

+ Use the useUser hook to access the user profile from protected client-side rendered pages. The{' '} + useUser hook relies on the UserProvider Context Provider, so you need to wrap your + custom App Component with it. +

+

+ You can also fetch the user profile by calling the /auth/me API route. +

- +
); -}); +} diff --git a/Sample-01/app/external/page.jsx b/Sample-01/app/external/page.jsx index 5b7316c..40784dc 100644 --- a/Sample-01/app/external/page.jsx +++ b/Sample-01/app/external/page.jsx @@ -2,15 +2,18 @@ import React, { useState } from 'react'; import { Button } from 'reactstrap'; -import { withPageAuthRequired } from '@auth0/nextjs-auth0/client'; - +import useAuthGuard from '../../hooks/useAuthGuard'; +import Highlight from '../../components/Highlight'; import Loading from '../../components/Loading'; import ErrorMessage from '../../components/ErrorMessage'; -import Highlight from '../../components/Highlight'; -function External() { +export default function External() { + const { isAuthenticated, user, guardComponent } = useAuthGuard(); const [state, setState] = useState({ isLoading: false, response: undefined, error: undefined }); + // If not authenticated or loading, show guard component + if (!isAuthenticated) return guardComponent; + const callApi = async () => { setState(previous => ({ ...previous, isLoading: true })); @@ -68,8 +71,3 @@ function External() { ); } - -export default withPageAuthRequired(External, { - onRedirecting: () => , - onError: error => {error.message} -}); diff --git a/Sample-01/app/layout.jsx b/Sample-01/app/layout.jsx index 5fe40a8..7167b6f 100644 --- a/Sample-01/app/layout.jsx +++ b/Sample-01/app/layout.jsx @@ -1,11 +1,14 @@ -'use client'; +'use client' import './globals.css'; import NavBar from '../components/NavBar'; import { Container } from 'reactstrap'; import Footer from '../components/Footer'; import React from 'react'; -import { UserProvider } from '@auth0/nextjs-auth0/client'; +import initFontAwesome from '../utils/initFontAwesome'; + +// Initialize FontAwesome icons +initFontAwesome(); export default function RootLayout({ children }) { return ( @@ -20,13 +23,11 @@ export default function RootLayout({ children }) { -
{children}
-
); diff --git a/Sample-01/app/profile/page.jsx b/Sample-01/app/profile/page.jsx index 3286ba0..8279dff 100644 --- a/Sample-01/app/profile/page.jsx +++ b/Sample-01/app/profile/page.jsx @@ -2,47 +2,37 @@ import React from 'react'; import { Row, Col } from 'reactstrap'; -import { useUser, withPageAuthRequired } from '@auth0/nextjs-auth0/client'; - -import Loading from '../../components/Loading'; -import ErrorMessage from '../../components/ErrorMessage'; +import useAuthGuard from '../../hooks/useAuthGuard'; import Highlight from '../../components/Highlight'; -function Profile() { - const { user, isLoading } = useUser(); +export default function Profile() { + const { isAuthenticated, user, guardComponent } = useAuthGuard(); + + // If not authenticated or loading, show guard component + if (!isAuthenticated) return guardComponent; return ( <> - {isLoading && } - {user && ( - <> - - - Profile - - -

{user.name}

-

- {user.email} -

- -
- - {JSON.stringify(user, null, 2)} - - - )} + + + Profile + + +

{user.name}

+

+ {user.email} +

+ +
+ + {JSON.stringify(user, null, 2)} + ); } - -export default withPageAuthRequired(Profile, { - onRedirecting: () => , - onError: error => {error.message} -}); diff --git a/Sample-01/app/ssr/page.jsx b/Sample-01/app/ssr/page.jsx index c29a65e..7700659 100644 --- a/Sample-01/app/ssr/page.jsx +++ b/Sample-01/app/ssr/page.jsx @@ -1,31 +1,40 @@ import React from 'react'; -import { getSession, withPageAuthRequired } from '@auth0/nextjs-auth0'; - +import { auth0 } from '../../lib/auth0'; import Highlight from '../../components/Highlight'; -export default withPageAuthRequired( - async function SSRPage() { - const { user } = await getSession(); - return ( - <> -
-

Server-side Rendered Page

-
-

- You can protect a server-side rendered page by wrapping it with withPageAuthRequired. Only - logged in users will be able to access it. If the user is logged out, they will be redirected to the login - page instead.{' '} -

-
+export default async function SSRPage() { + const session = await auth0.getSession(); + + // Redirect to login if not authenticated + if (!session) { + return { + redirect: { + destination: '/auth/login', + permanent: false, + }, + }; + } + + const { user } = session; + + return ( + <> +
+

Server-side Rendered Page

+
+

+ You can protect a server-side rendered page by checking the session. Only + logged in users will be able to access it. If the user is logged out, they will be redirected to the login + page instead.{' '} +

-
-
-
User
- {JSON.stringify(user, null, 2)} -
+
+
+
+
User
+ {JSON.stringify(user, null, 2)}
- - ); - }, - { returnTo: '/ssr' } -); +
+ + ); +} diff --git a/Sample-01/components/NavBar.jsx b/Sample-01/components/NavBar.jsx index bf4eea4..369049f 100644 --- a/Sample-01/components/NavBar.jsx +++ b/Sample-01/components/NavBar.jsx @@ -14,7 +14,7 @@ import { DropdownMenu, DropdownItem } from 'reactstrap'; -import { useUser } from '@auth0/nextjs-auth0/client'; +import { useUser } from '@auth0/nextjs-auth0'; import PageLink from './PageLink'; import AnchorLink from './AnchorLink'; @@ -61,7 +61,7 @@ const NavBar = () => { {!isLoading && !user && ( @@ -92,7 +92,7 @@ const NavBar = () => { - + Log out @@ -103,7 +103,7 @@ const NavBar = () => { {!isLoading && !user && (