├── .circleci └── config.yml ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── Bug Report.yml │ ├── Feature Request.yml │ └── config.yml ├── dependabot.yml ├── stale.yml └── workflows │ └── semgrep.yml ├── .gitignore ├── LICENSE ├── README.md └── Sample-01 ├── .babelrc ├── .dockerignore ├── .env.local.example ├── .gitignore ├── .prettierrc ├── Dockerfile ├── README.md ├── api-server.js ├── app ├── api │ └── shows │ │ └── route.js ├── csr │ └── page.jsx ├── external │ └── page.jsx ├── globals.css ├── layout.jsx ├── page.jsx ├── profile │ └── page.jsx └── ssr │ └── page.jsx ├── components ├── AnchorLink.jsx ├── Content.jsx ├── ErrorMessage.jsx ├── Footer.jsx ├── Hero.jsx ├── Highlight.jsx ├── Layout.jsx ├── Loading.jsx ├── Logo.jsx ├── NavBar.jsx ├── NavBarItem.jsx └── PageLink.jsx ├── cypress.config.js ├── cypress ├── e2e │ ├── logged-in.cy.js │ └── logged-out.cy.js └── support │ ├── commands.js │ └── e2e.js ├── exec.ps1 ├── exec.sh ├── lib └── auth0.js ├── middleware.js ├── next.config.js ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── tests ├── app │ ├── api │ │ └── shows.test.js │ ├── csr.test.jsx │ ├── external.test.jsx │ ├── index.test.jsx │ ├── profile.test.jsx │ └── ssr.test.jsx ├── components │ ├── AnchorLink.test.jsx │ ├── Content.test.jsx │ ├── ErrorMessage.test.jsx │ ├── Footer.test.jsx │ ├── Hero.test.jsx │ ├── Highlight.test.jsx │ ├── Layout.test.jsx │ ├── Loading.test.jsx │ ├── Logo.test.jsx │ ├── NavBar.test.jsx │ ├── NavBarItem.test.jsx │ └── PageLink.test.jsx ├── environment.js ├── fixtures.jsx └── setup.js ├── utils ├── contentData.js └── initFontAwesome.js └── vitest.config.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | browser-tools: circleci/browser-tools@1 5 | 6 | jobs: 7 | Sample-01: 8 | working_directory: ~/samples/Sample-01 9 | docker: 10 | - image: cimg/node:lts-browsers 11 | steps: 12 | - checkout: 13 | path: ~/samples 14 | - restore_cache: 15 | key: dependencies-{{ .Branch }}-{{ checksum "package-lock.json" }} 16 | - run: npm ci 17 | - save_cache: 18 | key: dependencies-{{ .Branch }}-{{ checksum "package-lock.json" }} 19 | paths: 20 | - ~/.npm 21 | - ~/.cache 22 | - run: npm run build 23 | - run: npm test 24 | - browser-tools/install-chrome 25 | - run: npm run test:integration 26 | - store_test_results: 27 | path: test-results 28 | - store_artifacts: 29 | path: cypress/videos 30 | - store_artifacts: 31 | path: cypress/screenshots 32 | 33 | workflows: 34 | Build and Test: 35 | jobs: 36 | - Sample-01 37 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @auth0-samples/dx-sdks-engineer 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug Report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Report a bug 2 | description: Have you found a bug or issue? Create a bug report for this sample 3 | 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | **Please do not report security vulnerabilities here**. The [Responsible Disclosure Program](https://auth0.com/responsible-disclosure-policy) details the procedure for disclosing security issues. 9 | 10 | - type: checkboxes 11 | id: checklist 12 | attributes: 13 | label: Checklist 14 | options: 15 | - label: I have looked into the [Readme](https://github.com/auth0-samples/auth0-nextjs-samples/tree/main/Sample-01#readme) and have not found a suitable solution or answer. 16 | required: true 17 | - label: I have searched the [issues](https://github.com/auth0-samples/auth0-nextjs-samples/issues) and have not found a suitable solution or answer. 18 | required: true 19 | - label: I have searched the [Auth0 Community](https://community.auth0.com) forums and have not found a suitable solution or answer. 20 | required: true 21 | - label: I agree to the terms within the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md). 22 | required: true 23 | 24 | - type: textarea 25 | id: description 26 | attributes: 27 | label: Description 28 | description: Provide a clear and concise description of the issue, including what you expected to happen. 29 | validations: 30 | required: true 31 | 32 | - type: textarea 33 | id: reproduction 34 | attributes: 35 | label: Reproduction 36 | description: Detail the steps taken to reproduce this error, and whether this issue can be reproduced consistently or if it is intermittent. 37 | placeholder: | 38 | 1. Step 1... 39 | 2. Step 2... 40 | 3. ... 41 | validations: 42 | required: true 43 | 44 | - type: textarea 45 | id: additional-context 46 | attributes: 47 | label: Additional context 48 | description: Any other relevant information you think would be useful. 49 | validations: 50 | required: false 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature Request.yml: -------------------------------------------------------------------------------- 1 | name: 🧩 Feature request 2 | description: Suggest an idea or a feature for this sample 3 | labels: ["feature request"] 4 | 5 | body: 6 | - type: checkboxes 7 | id: checklist 8 | attributes: 9 | label: Checklist 10 | options: 11 | - label: I have looked into the [Readme](https://github.com/auth0-samples/auth0-nextjs-samples/tree/main/Sample-01#readme) and have not found a suitable solution or answer. 12 | required: true 13 | - label: I have searched the [issues](https://github.com/auth0-samples/auth0-nextjs-samples/issues) and have not found a suitable solution or answer. 14 | required: true 15 | - label: I have searched the [Auth0 Community](https://community.auth0.com) forums and have not found a suitable solution or answer. 16 | required: true 17 | - label: I agree to the terms within the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md). 18 | required: true 19 | 20 | - type: textarea 21 | id: description 22 | attributes: 23 | label: Describe the problem you'd like to have solved 24 | description: A clear and concise description of what the problem is. 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | id: ideal-solution 30 | attributes: 31 | label: Describe the ideal solution 32 | description: A clear and concise description of what you want to happen. 33 | validations: 34 | required: true 35 | 36 | - type: textarea 37 | id: alternatives-and-workarounds 38 | attributes: 39 | label: Alternatives and current workarounds 40 | description: A clear and concise description of any alternatives you've considered or any workarounds that are currently in place. 41 | validations: 42 | required: false 43 | 44 | - type: textarea 45 | id: additional-context 46 | attributes: 47 | label: Additional context 48 | description: Add any other context or screenshots about the feature request here. 49 | validations: 50 | required: false 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 🤔 Help & Questions 4 | url: https://community.auth0.com 5 | about: Ask general support or usage questions in the Auth0 Community forums. 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "npm" 5 | directory: "/Sample-01" 6 | schedule: 7 | interval: "daily" 8 | ignore: 9 | - dependency-name: "*" 10 | update-types: ["version-update:semver-major", "version-update:semver-patch"] 11 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 90 5 | 6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 7 | daysUntilClose: 7 8 | 9 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 10 | exemptLabels: [] 11 | 12 | # Set to true to ignore issues with an assignee (defaults to false) 13 | exemptAssignees: true 14 | 15 | # Label to use when marking as stale 16 | staleLabel: closed:stale 17 | 18 | # Comment to post when marking as stale. Set to `false` to disable 19 | markComment: > 20 | This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If you have not received a response for our team (apologies for the delay) and this is still a blocker, please reply with additional information or just a ping. Thank you for your contribution! 🙇‍♂️ 21 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | name: Semgrep 2 | 3 | on: 4 | pull_request: {} 5 | 6 | push: 7 | branches: ["master", "main"] 8 | 9 | schedule: 10 | - cron: '30 0 1,15 * *' 11 | 12 | jobs: 13 | semgrep: 14 | name: Scan 15 | runs-on: ubuntu-latest 16 | container: 17 | image: returntocorp/semgrep 18 | # Skip any PR created by dependabot to avoid permission issues 19 | if: (github.actor != 'dependabot[bot]') 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - run: semgrep ci 24 | env: 25 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # misc 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Auth0, Inc. (http://auth0.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Auth0 Next.js Samples 2 | 3 | [![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) 4 | [![License](https://img.shields.io/:license-mit-blue.svg?style=flat)](https://opensource.org/licenses/MIT) 5 | 6 | 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). 7 | 8 | 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. 9 | 10 | ## What is Auth0? 11 | 12 | Auth0 helps you to: 13 | 14 | * Add authentication with [multiple sources](https://auth0.com/docs/identityproviders), either social identity providers such as **Google, Facebook, Microsoft Account, LinkedIn, GitHub, Twitter, Box, Salesforce** (amongst others), or enterprise identity systems like **Windows Azure AD, Google Apps, Active Directory, ADFS, or any SAML Identity Provider**. 15 | * Add authentication through more traditional **[username/password databases](https://auth0.com/docs/connections/database/custom-db)**. 16 | * Add support for **[linking different user accounts](https://auth0.com/docs/users/user-account-linking)** with the same user. 17 | * Support for generating signed [JSON Web Tokens](https://auth0.com/docs/tokens/json-web-tokens) to call your APIs and **flow the user identity** securely. 18 | * Analytics of how, when, and where users are logging in. 19 | * Pull data from other sources and add it to the user profile through [JavaScript rules](https://auth0.com/docs/rules). 20 | 21 | ## Create a Free Auth0 Account 22 | 23 | 1. Go to [Auth0](https://auth0.com) and click **Sign Up**. 24 | 2. Use Google, GitHub, or Microsoft Account to login. 25 | 26 | ## Issue Reporting 27 | 28 | If you have found a bug or if you have a feature request, please report them at this repository issues section. Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/responsible-disclosure-policy) details the procedure for disclosing security issues. 29 | 30 | ## Author 31 | 32 | [Auth0](https://auth0.com) 33 | 34 | ## License 35 | 36 | This project is licensed under the MIT license. See the [LICENSE](./LICENSE) file for more info. 37 | -------------------------------------------------------------------------------- /Sample-01/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"] 3 | } 4 | -------------------------------------------------------------------------------- /Sample-01/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | exec.* 4 | npm-debug.log* 5 | yarn-error.log 6 | README.md 7 | 8 | # testing 9 | /coverage 10 | 11 | # misc 12 | .DS_Store 13 | .env.development.local 14 | .env.test.local 15 | .env.production.local 16 | 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | -------------------------------------------------------------------------------- /Sample-01/.env.local.example: -------------------------------------------------------------------------------- 1 | AUTH0_SECRET=replace-with-your-own-secret-generated-with-openssl 2 | APP_BASE_URL=http://localhost:3000 3 | AUTH0_DOMAIN='{DOMAIN}' 4 | AUTH0_CLIENT_ID='{CLIENT_ID}' 5 | AUTH0_CLIENT_SECRET='{CLIENT_SECRET}' 6 | AUTH0_AUDIENCE= 7 | AUTH0_SCOPE='openid profile' 8 | -------------------------------------------------------------------------------- /Sample-01/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # Cypress 12 | test-results 13 | cypress/screenshots 14 | cypress/videos 15 | cypress.env.json 16 | 17 | # next.js 18 | /.next/ 19 | /out/ 20 | 21 | # production 22 | /build 23 | 24 | # misc 25 | .DS_Store 26 | *.pem 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | 33 | # local env files 34 | .env.local 35 | .env.development.local 36 | .env.test.local 37 | .env.production.local 38 | 39 | # vercel 40 | .vercel 41 | -------------------------------------------------------------------------------- /Sample-01/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 120, 4 | "trailingComma": "none", 5 | "tabWidth": 2, 6 | "arrowParens": "avoid", 7 | "jsxBracketSameLine": true 8 | } -------------------------------------------------------------------------------- /Sample-01/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine as build 2 | 3 | RUN mkdir /app 4 | 5 | WORKDIR /app 6 | 7 | COPY package.json . 8 | COPY next.config.js . 9 | COPY api-server.js . 10 | COPY .env.local . 11 | 12 | RUN npm install 13 | RUN npm build 14 | 15 | COPY .next ./.next 16 | COPY public ./.public 17 | COPY . . 18 | 19 | # --------------- 20 | 21 | FROM node:lts-alpine 22 | 23 | ENV NODE_ENV production 24 | ENV API_PORT 3001 25 | 26 | WORKDIR /app 27 | 28 | COPY --from=build /app/package.json . 29 | COPY --from=build /app/next.config.js . 30 | COPY --from=build /app/api-server.js . 31 | COPY --from=build /app/.env.local . 32 | COPY --from=build /app/.next ./.next 33 | COPY --from=build /app/public ./public 34 | 35 | RUN npm install 36 | 37 | EXPOSE 3000 38 | EXPOSE 3001 39 | 40 | CMD npm start 41 | -------------------------------------------------------------------------------- /Sample-01/README.md: -------------------------------------------------------------------------------- 1 | # Auth0 Next.js SDK Sample Application 2 | 3 | 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). 4 | 5 | This sample demonstrates the following use cases: 6 | 7 | - [Login](https://github.com/auth0-samples/auth0-nextjs-samples/blob/main/Sample-01/components/NavBar.jsx#L61-L67) 8 | - [Logout](https://github.com/auth0-samples/auth0-nextjs-samples/blob/main/Sample-01/components/NavBar.jsx#L93-L95) 9 | - [Showing the user profile](https://github.com/auth0-samples/auth0-nextjs-samples/blob/main/Sample-01/pages/profile.jsx) 10 | - [Protecting client-side rendered pages](https://github.com/auth0-samples/auth0-nextjs-samples/blob/main/Sample-01/pages/profile.jsx#L43-L46) 11 | - [Calling APIs](https://github.com/auth0-samples/auth0-nextjs-samples/blob/main/Sample-01/pages/external.jsx) 12 | 13 | ## Project setup 14 | 15 | Use `npm` to install the project dependencies: 16 | 17 | ```bash 18 | npm install 19 | ``` 20 | 21 | ## Configuration 22 | 23 | ### Create an API 24 | 25 | 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. 26 | 27 | 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. 28 | 29 | ### Configure credentials 30 | 31 | The project needs to be configured with your Auth0 Domain, Client ID and Client Secret for the authentication flow to work. 32 | 33 | 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)): 34 | 35 | ```sh 36 | # A long secret value used to encrypt the session cookie 37 | AUTH0_SECRET='LONG_RANDOM_VALUE' 38 | # The base url of your application 39 | APP_BASE_URL='http://localhost:3000' 40 | # Your Auth0 tenant domain 41 | AUTH0_DOMAIN='YOUR_AUTH0_DOMAIN.auth0.com' 42 | # Your Auth0 application's Client ID 43 | AUTH0_CLIENT_ID='YOUR_AUTH0_CLIENT_ID' 44 | # Your Auth0 application's Client Secret 45 | AUTH0_CLIENT_SECRET='YOUR_AUTH0_CLIENT_SECRET' 46 | # Your Auth0 API's Identifier 47 | # OMIT if you do not want to use the API part of the sample 48 | AUTH0_AUDIENCE='YOUR_AUTH0_API_IDENTIFIER' 49 | # The permissions your app is asking for 50 | # OMIT if you do not want to use the API part of the sample 51 | AUTH0_SCOPE='openid profile email read:shows' 52 | ``` 53 | 54 | **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). 55 | 56 | ## Run the sample 57 | 58 | ### Compile and hot-reload for development 59 | 60 | This compiles and serves the Next.js app and starts the API server on port 3001. 61 | 62 | ```bash 63 | npm run dev 64 | ``` 65 | 66 | ## Deployment 67 | 68 | ### Compiles and minifies for production 69 | 70 | ```bash 71 | npm run build 72 | ``` 73 | 74 | ### Docker build 75 | 76 | To build and run the Docker image, run `exec.sh`, or `exec.ps1` on Windows. 77 | 78 | ### Run the unit tests 79 | 80 | ```bash 81 | npm run test 82 | ``` 83 | 84 | ### Run the integration tests 85 | 86 | ```bash 87 | npm run test:integration 88 | ``` 89 | 90 | ## What is Auth0? 91 | 92 | Auth0 helps you to: 93 | 94 | * Add authentication with [multiple sources](https://auth0.com/docs/identityproviders), either social identity providers such as **Google, Facebook, Microsoft Account, LinkedIn, GitHub, Twitter, Box, Salesforce** (amongst others), or enterprise identity systems like **Windows Azure AD, Google Apps, Active Directory, ADFS, or any SAML Identity Provider**. 95 | * Add authentication through more traditional **[username/password databases](https://auth0.com/docs/connections/database/custom-db)**. 96 | * Add support for **[linking different user accounts](https://auth0.com/docs/users/user-account-linking)** with the same user. 97 | * Support for generating signed [JSON Web Tokens](https://auth0.com/docs/tokens/json-web-tokens) to call your APIs and **flow the user identity** securely. 98 | * Analytics of how, when, and where users are logging in. 99 | * Pull data from other sources and add it to the user profile through [JavaScript rules](https://auth0.com/docs/rules). 100 | 101 | ## Create a Free Auth0 Account 102 | 103 | 1. Go to [Auth0](https://auth0.com) and click **Sign Up**. 104 | 2. Use Google, GitHub, or Microsoft Account to login. 105 | 106 | ## Issue Reporting 107 | 108 | If you have found a bug or if you have a feature request, please report them at this repository issues section. Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/responsible-disclosure-policy) details the procedure for disclosing security issues. 109 | 110 | ## Author 111 | 112 | [Auth0](https://auth0.com) 113 | 114 | ## License 115 | 116 | This project is licensed under the MIT license. See the [LICENSE](./LICENSE) file for more info. 117 | -------------------------------------------------------------------------------- /Sample-01/api-server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ path: './.env.local' }); 2 | 3 | const express = require('express'); 4 | const cors = require('cors'); 5 | const morgan = require('morgan'); 6 | const helmet = require('helmet'); 7 | const { expressjwt: jwt } = require('express-jwt'); 8 | const jwksRsa = require('jwks-rsa'); 9 | 10 | const app = express(); 11 | const port = process.env.API_PORT || 3001; 12 | const baseUrl = process.env.APP_BASE_URL; 13 | const domain = process.env.AUTH0_DOMAIN; 14 | const issuerBaseUrl = `https://${domain}`; 15 | const audience = process.env.AUTH0_AUDIENCE; 16 | 17 | if (!baseUrl || !domain) { 18 | throw new Error('Please make sure that the file .env.local is in place and populated'); 19 | } 20 | 21 | if (!audience) { 22 | console.log('AUTH0_AUDIENCE not set in .env.local. Shutting down API server.'); 23 | process.exit(1); 24 | } 25 | 26 | app.use(morgan('dev')); 27 | app.use(helmet()); 28 | app.use(cors({ origin: baseUrl })); 29 | 30 | const checkJwt = jwt({ 31 | secret: jwksRsa.expressJwtSecret({ 32 | cache: true, 33 | rateLimit: true, 34 | jwksRequestsPerMinute: 5, 35 | jwksUri: `${issuerBaseUrl}/.well-known/jwks.json` 36 | }), 37 | audience: audience, 38 | issuer: `${issuerBaseUrl}/`, 39 | algorithms: ['RS256'] 40 | }); 41 | 42 | app.get('/api/shows', checkJwt, (req, res) => { 43 | res.send({ 44 | msg: 'Your access token was successfully validated!' 45 | }); 46 | }); 47 | 48 | const server = app.listen(port, () => console.log(`API Server listening on port ${port}`)); 49 | process.on('SIGINT', () => server.close()); 50 | -------------------------------------------------------------------------------- /Sample-01/app/api/shows/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { auth0 } from '../../../lib/auth0'; 3 | 4 | export const GET = async function shows() { 5 | try { 6 | const session = await auth0.getSession(); 7 | 8 | if (!session) { 9 | return NextResponse.json( 10 | { error: 'Not authenticated' }, 11 | { status: 401 } 12 | ); 13 | } 14 | 15 | const res = new NextResponse(); 16 | const { token: accessToken } = await auth0.getAccessToken(); 17 | const apiPort = process.env.API_PORT || 3001; 18 | const response = await fetch(`http://localhost:${apiPort}/api/shows`, { 19 | headers: { 20 | Authorization: `Bearer ${accessToken}` 21 | } 22 | }); 23 | const shows = await response.json(); 24 | 25 | return NextResponse.json(shows, res); 26 | } catch (error) { 27 | return NextResponse.json({ error: error.message }, { status: error.status || 500 }); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /Sample-01/app/csr/page.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | export default function CSRPage() { 6 | return ( 7 | <> 8 |
9 |

Client-side Rendered Page

10 |
11 |

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

16 |

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

21 |

22 | You can also fetch the user profile by calling the /auth/profile API route. 23 |

24 |
25 |
26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /Sample-01/app/external/page.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useState } from 'react'; 4 | import { Button } from 'reactstrap'; 5 | 6 | import Loading from '../../components/Loading'; 7 | import ErrorMessage from '../../components/ErrorMessage'; 8 | import Highlight from '../../components/Highlight'; 9 | 10 | export default function External() { 11 | const [state, setState] = useState({ isLoading: false, response: undefined, error: undefined }); 12 | 13 | const callApi = async () => { 14 | setState(previous => ({ ...previous, isLoading: true })); 15 | 16 | try { 17 | const response = await fetch('/api/shows'); 18 | const data = await response.json(); 19 | 20 | setState(previous => ({ ...previous, response: data, error: undefined })); 21 | } catch (error) { 22 | setState(previous => ({ ...previous, response: undefined, error })); 23 | } finally { 24 | setState(previous => ({ ...previous, isLoading: false })); 25 | } 26 | }; 27 | 28 | const handle = (event, fn) => { 29 | event.preventDefault(); 30 | fn(); 31 | }; 32 | 33 | const { isLoading, response, error } = state; 34 | 35 | return ( 36 | <> 37 |
38 |

External API

39 |
40 |

Ping an external API by clicking the button below

41 |

42 | This will call a local API on port 3001 that would have been started if you run npm run dev. 43 |

44 |

45 | An access token is sent as part of the request's Authorization header and the API will validate 46 | it using the API's audience value. The audience is the identifier of the API that you want to call (see{' '} 47 | 48 | API Authorization Settings 49 | {' '} 50 | for more info). 51 |

52 |
53 | 56 |
57 |
58 | {isLoading && } 59 | {(error || response) && ( 60 |
61 |
Result
62 | {error && {error.message}} 63 | {response && {JSON.stringify(response, null, 2)}} 64 |
65 | )} 66 |
67 | 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /Sample-01/app/globals.css: -------------------------------------------------------------------------------- 1 | body { 2 | -webkit-font-smoothing: antialiased; 3 | -moz-osx-font-smoothing: grayscale; 4 | } 5 | 6 | code { 7 | white-space: pre-wrap; 8 | word-break: break-all; 9 | } 10 | 11 | p code { 12 | color: var(--secondary); 13 | font-weight: 600; 14 | } 15 | 16 | #__next { 17 | height: 100%; 18 | } 19 | 20 | #nav-mobile { 21 | min-height: 170px; 22 | } 23 | 24 | .nav-item.dropdown .dropdown-item { 25 | padding: 0; 26 | } 27 | 28 | .nav-item.dropdown .dropdown-item a { 29 | color: inherit; 30 | } 31 | 32 | .nav-item.dropdown .dropdown-item .navbar-item { 33 | width: 100%; 34 | padding: .55rem 1.5rem; 35 | } 36 | 37 | .result-block-container .result-block { 38 | opacity: 1; 39 | } 40 | -------------------------------------------------------------------------------- /Sample-01/app/layout.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import './globals.css'; 4 | import NavBar from '../components/NavBar'; 5 | import { Container } from 'reactstrap'; 6 | import Footer from '../components/Footer'; 7 | import React from 'react'; 8 | import { Auth0Provider } from '@auth0/nextjs-auth0'; 9 | 10 | export default function RootLayout({ children }) { 11 | return ( 12 | 13 | 14 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | {children} 27 |
28 |
29 |
30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /Sample-01/app/page.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | import Hero from '../components/Hero'; 6 | import Content from '../components/Content'; 7 | 8 | export default function Index() { 9 | return ( 10 | <> 11 | 12 |
13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /Sample-01/app/profile/page.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { Row, Col } from 'reactstrap'; 5 | import { useUser } from '@auth0/nextjs-auth0'; 6 | 7 | import Loading from '../../components/Loading'; 8 | import Highlight from '../../components/Highlight'; 9 | 10 | export default function Profile() { 11 | const { user, isLoading } = useUser(); 12 | 13 | return ( 14 | <> 15 | {isLoading && } 16 | {user && ( 17 | <> 18 | 19 | 20 | Profile 27 | 28 | 29 |

{user.name}

30 |

31 | {user.email} 32 |

33 | 34 |
35 | 36 | {JSON.stringify(user, null, 2)} 37 | 38 | 39 | )} 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /Sample-01/app/ssr/page.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { auth0 } from '../../lib/auth0'; 3 | 4 | import Highlight from '../../components/Highlight'; 5 | 6 | export default async function SSRPage() { 7 | const { user } = await auth0.getSession(); 8 | return ( 9 | <> 10 |
11 |

Server-side Rendered Page

12 |
13 |

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

18 |
19 |
20 |
21 |
22 |
User
23 | {JSON.stringify(user, null, 2)} 24 |
25 |
26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /Sample-01/components/AnchorLink.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import NavBarItem from './NavBarItem'; 4 | 5 | const AnchorLink = ({ children, href, className, icon, tabIndex, testId }) => { 6 | return ( 7 | 8 | 9 | {children} 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default AnchorLink; 16 | -------------------------------------------------------------------------------- /Sample-01/components/Content.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Row, Col } from 'reactstrap'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | 5 | import contentData from '../utils/contentData'; 6 | 7 | const Content = () => ( 8 |
9 |

10 | What can I do next? 11 |

12 | 13 | {contentData.map((col, i) => ( 14 | 15 |
16 | 17 | 18 | {col.title} 19 | 20 |
21 |

{col.description}

22 | 23 | ))} 24 |
25 |
26 | ); 27 | 28 | export default Content; 29 | -------------------------------------------------------------------------------- /Sample-01/components/ErrorMessage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Alert } from 'reactstrap'; 3 | 4 | const ErrorMessage = ({ children }) => ( 5 | 6 | {children} 7 | 8 | ); 9 | 10 | export default ErrorMessage; 11 | -------------------------------------------------------------------------------- /Sample-01/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Footer = () => ( 4 |
5 |
6 |

7 | Sample project provided by Auth0 8 |

9 |
10 | ); 11 | 12 | export default Footer; 13 | -------------------------------------------------------------------------------- /Sample-01/components/Hero.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Logo from './Logo'; 4 | 5 | const Hero = () => ( 6 |
7 | 8 |

9 | Next.js Sample Project 10 |

11 | 12 |

13 | This is a sample application that demonstrates an authentication flow for a Regular Web App, using{' '} 14 | Next.js 15 |

16 |
17 | ); 18 | 19 | export default Hero; 20 | -------------------------------------------------------------------------------- /Sample-01/components/Highlight.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect, useState, useRef } from 'react'; 4 | import hljs from 'highlight.js/lib/core'; 5 | import json from 'highlight.js/lib/languages/json'; 6 | 7 | import 'highlight.js/styles/monokai-sublime.css'; 8 | 9 | const Highlight = ({ children, testId }) => { 10 | const [isLoaded, setIsLoaded] = useState(false); 11 | const codeNode = useRef(); 12 | const language = 'json'; 13 | 14 | useEffect(() => { 15 | try { 16 | hljs.registerLanguage(language, json); 17 | setIsLoaded(true); 18 | } catch (error) { 19 | console.error(error); 20 | throw Error(`Cannot register the language ${language}`); 21 | } 22 | }, []); 23 | 24 | useEffect(() => { 25 | codeNode && codeNode.current && hljs.highlightElement(codeNode.current); 26 | }); 27 | 28 | if (!isLoaded) return null; 29 | 30 | return ( 31 |
32 |       
33 |         {children}
34 |       
35 |     
36 | ); 37 | }; 38 | 39 | export default Highlight; 40 | -------------------------------------------------------------------------------- /Sample-01/components/Layout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Container } from 'reactstrap'; 3 | import Head from 'next/head'; 4 | 5 | import NavBar from './NavBar'; 6 | import Footer from './Footer'; 7 | 8 | const Layout = ({ children }) => ( 9 | <> 10 | 11 | 17 | 18 | Next.js Sample App 19 | 20 |
21 | 22 | {children} 23 |
24 |
25 | 26 | ); 27 | 28 | export default Layout; 29 | -------------------------------------------------------------------------------- /Sample-01/components/Loading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Loading = () => ( 4 |
5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 31 | 32 | 33 |
34 | ); 35 | 36 | export default Loading; 37 | -------------------------------------------------------------------------------- /Sample-01/components/Logo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Logo = ({ testId }) => ( 4 |
5 | 6 | 7 | 8 |
9 | ); 10 | 11 | export default Logo; 12 | -------------------------------------------------------------------------------- /Sample-01/components/NavBar.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useState } from 'react'; 4 | import { 5 | Collapse, 6 | Container, 7 | Navbar, 8 | NavbarToggler, 9 | NavbarBrand, 10 | Nav, 11 | NavItem, 12 | UncontrolledDropdown, 13 | DropdownToggle, 14 | DropdownMenu, 15 | DropdownItem 16 | } from 'reactstrap'; 17 | import { useUser } from '@auth0/nextjs-auth0'; 18 | 19 | import PageLink from './PageLink'; 20 | import AnchorLink from './AnchorLink'; 21 | 22 | const NavBar = () => { 23 | const [isOpen, setIsOpen] = useState(false); 24 | const { user, isLoading } = useUser(); 25 | const toggle = () => setIsOpen(!isOpen); 26 | 27 | return ( 28 |
29 | 30 | 31 | 32 | 33 | 34 | 60 | 103 | {!isLoading && !user && ( 104 | 113 | )} 114 | {user && ( 115 | 151 | )} 152 | 153 | 154 | 155 |
156 | ); 157 | }; 158 | 159 | export default NavBar; 160 | -------------------------------------------------------------------------------- /Sample-01/components/NavBarItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { usePathname } from 'next/navigation'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | 5 | const NavBarItem = ({ children, href, className, icon, tabIndex, testId }) => { 6 | const pathname = usePathname(); 7 | const activeClass = 'navbar-item-active'; 8 | const activeClasses = className ? `${className} ${activeClass}` : activeClass; 9 | 10 | return ( 11 | 12 | {icon && } 13 | 14 | {children} 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default NavBarItem; 21 | -------------------------------------------------------------------------------- /Sample-01/components/PageLink.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | 4 | import NavBarItem from './NavBarItem'; 5 | 6 | const PageLink = ({ children, href, className, icon, tabIndex, testId }) => { 7 | return ( 8 | 9 | 10 | 11 | {children} 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default PageLink; 19 | -------------------------------------------------------------------------------- /Sample-01/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress'); 2 | 3 | module.exports = defineConfig({ 4 | chromeWebSecurity: false, 5 | viewportWidth: 1000, 6 | viewportHeight: 1000, 7 | fixturesFolder: false, 8 | reporter: 'junit', 9 | reporterOptions: { 10 | mochaFile: 'test-results/cypress/junit-[hash].xml' 11 | }, 12 | retries: { 13 | runMode: 3 14 | }, 15 | e2e: { 16 | setupNodeEvents(on, config) {}, 17 | experimentalSessionAndOrigin: true, 18 | baseUrl: 'http://localhost:3000' 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /Sample-01/cypress/e2e/logged-in.cy.js: -------------------------------------------------------------------------------- 1 | const EMAIL = Cypress.env('USER_EMAIL'); 2 | const PASSWORD = Cypress.env('USER_PASSWORD'); 3 | 4 | if (!EMAIL || !PASSWORD) { 5 | throw new Error('You must provide CYPRESS_USER_EMAIL and CYPRESS_USER_PASSWORD environment variables'); 6 | } 7 | 8 | const login = () => { 9 | cy.get('input[name=email], input[name=username]').focus().clear().type(EMAIL); 10 | cy.get('input[name=password]').focus().clear().type(PASSWORD, { log: false }); 11 | cy.get('button[type=submit][name=action]:visible, button[type=submit][name=submit]').click(); 12 | cy.url().should('equal', 'http://localhost:3000/'); 13 | cy.visit('/'); 14 | }; 15 | 16 | describe('logged in', () => { 17 | context('desktop', () => { 18 | beforeEach(() => { 19 | cy.visit('/'); 20 | cy.get('[data-testid=navbar-login-desktop]').click(); 21 | login(); 22 | }); 23 | 24 | it('should display the navigation bar', () => { 25 | cy.get('[data-testid=navbar]').should('be.visible'); 26 | cy.get('[data-testid=navbar-items]').should('be.visible'); 27 | cy.get('[data-testid=navbar-menu-desktop]').should('be.visible'); 28 | cy.get('[data-testid=navbar-menu-mobile]').should('not.be.visible'); 29 | cy.get('[data-testid=navbar-picture-desktop]').should('be.visible'); 30 | cy.get('[data-testid=navbar-picture-mobile]').should('not.be.visible'); 31 | }); 32 | 33 | it('should expand the navigation bar menu', () => { 34 | cy.get('[data-testid=navbar-user-desktop]').should('not.be.visible'); 35 | cy.get('[data-testid=navbar-profile-desktop]').should('not.be.visible'); 36 | cy.get('[data-testid=navbar-logout-desktop]').should('not.be.visible'); 37 | cy.get('[data-testid=navbar-menu-desktop]').click(); 38 | cy.get('[data-testid=navbar-user-desktop]').should('be.visible'); 39 | cy.get('[data-testid=navbar-profile-desktop]').should('be.visible'); 40 | cy.get('[data-testid=navbar-logout-desktop]').should('be.visible'); 41 | }); 42 | 43 | it('should display the footer', () => { 44 | cy.get('[data-testid=footer]').should('be.visible'); 45 | }); 46 | 47 | it('should display the home page', () => { 48 | cy.get('[data-testid=navbar-home]').click(); 49 | cy.url().should('eq', `${Cypress.config().baseUrl}/`); 50 | 51 | cy.get('[data-testid=navbar-home]').isActive(); 52 | cy.get('[data-testid=hero]').should('be.visible'); 53 | cy.get('[data-testid=content]').should('be.visible'); 54 | }); 55 | 56 | it('should display the client-side rendered page', () => { 57 | cy.get('[data-testid=navbar-csr]').click(); 58 | cy.url().should('eq', `${Cypress.config().baseUrl}/csr`); 59 | 60 | cy.get('[data-testid=navbar-csr]').isActive(); 61 | cy.get('[data-testid=csr]').should('be.visible'); 62 | }); 63 | 64 | it('should display the server-side rendered page', () => { 65 | cy.get('[data-testid=navbar-ssr]').click(); 66 | cy.url().should('eq', `${Cypress.config().baseUrl}/ssr`); 67 | 68 | cy.get('[data-testid=navbar-ssr]').isActive(); 69 | cy.get('[data-testid=ssr]').should('be.visible'); 70 | cy.get('[data-testid=ssr-json]').contains(EMAIL); 71 | }); 72 | 73 | it('should display the external API page', () => { 74 | cy.get('[data-testid=navbar-external]').click(); 75 | cy.url().should('eq', `${Cypress.config().baseUrl}/external`); 76 | 77 | cy.get('[data-testid=navbar-external]').isActive(); 78 | cy.get('[data-testid=external]').should('be.visible'); 79 | }); 80 | 81 | it('should display the external API result', () => { 82 | cy.get('[data-testid=navbar-external]').click(); 83 | cy.get('[data-testid=external-action]').click(); 84 | cy.get('[data-testid=external-result]').should('be.visible'); 85 | }); 86 | 87 | it('should display the profile page', () => { 88 | cy.get('[data-testid=navbar-menu-desktop]').click(); 89 | cy.get('[data-testid=navbar-profile-desktop]').click(); 90 | cy.url().should('eq', `${Cypress.config().baseUrl}/profile`); 91 | 92 | cy.get('[data-testid=profile]').should('be.visible'); 93 | cy.get('[data-testid=profile-email]').contains(EMAIL); 94 | cy.get('[data-testid=profile-json]').contains(EMAIL); 95 | }); 96 | }); 97 | 98 | context('mobile', () => { 99 | beforeEach(() => { 100 | cy.mobileViewport(); 101 | cy.visit('/'); 102 | cy.get('[data-testid=navbar-toggle]').click(); 103 | cy.get('[data-testid=navbar-login-mobile]').click(); 104 | login(); 105 | }); 106 | 107 | it('should expand the navigation bar menu', () => { 108 | cy.get('[data-testid=navbar-items]').should('not.be.visible'); 109 | cy.get('[data-testid=navbar-menu-mobile]').should('not.be.visible'); 110 | cy.get('[data-testid=navbar-picture-mobile]').should('not.be.visible'); 111 | cy.get('[data-testid=navbar-user-mobile]').should('not.be.visible'); 112 | cy.get('[data-testid=navbar-profile-mobile]').should('not.be.visible'); 113 | cy.get('[data-testid=navbar-logout-mobile]').should('not.be.visible'); 114 | cy.get('[data-testid=navbar-toggle]').click(); 115 | cy.get('[data-testid=navbar-items]').should('be.visible'); 116 | cy.get('[data-testid=navbar-menu-mobile]').should('be.visible'); 117 | cy.get('[data-testid=navbar-picture-mobile]').should('be.visible'); 118 | cy.get('[data-testid=navbar-user-mobile]').should('be.visible'); 119 | cy.get('[data-testid=navbar-profile-mobile]').should('be.visible'); 120 | cy.get('[data-testid=navbar-logout-mobile]').should('be.visible'); 121 | }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /Sample-01/cypress/e2e/logged-out.cy.js: -------------------------------------------------------------------------------- 1 | describe('logged out', () => { 2 | beforeEach(() => cy.visit('/')); 3 | 4 | context('desktop', () => { 5 | it('should display the navigation bar', () => { 6 | cy.get('[data-testid=navbar]').should('be.visible'); 7 | cy.get('[data-testid=navbar-items]').should('be.visible'); 8 | cy.get('[data-testid=navbar-login-desktop]').should('be.visible'); 9 | cy.get('[data-testid=navbar-login-mobile]').should('not.be.visible'); 10 | cy.get('[data-testid=navbar-toggle]').should('not.be.visible'); 11 | }); 12 | 13 | it('should display the footer', () => { 14 | cy.get('[data-testid=footer]').should('be.visible'); 15 | }); 16 | 17 | it('should display the home page', () => { 18 | cy.get('[data-testid=navbar-home]').click(); 19 | cy.url().should('eq', `${Cypress.config().baseUrl}/`); 20 | 21 | cy.get('[data-testid=navbar-home]').isActive(); 22 | cy.get('[data-testid=hero]').should('be.visible'); 23 | cy.get('[data-testid=content]').should('be.visible'); 24 | }); 25 | }); 26 | 27 | context('mobile', () => { 28 | beforeEach(() => cy.mobileViewport()); 29 | 30 | it('should expand the navigation bar menu', () => { 31 | cy.get('[data-testid=navbar-items]').should('not.be.visible'); 32 | cy.get('[data-testid=navbar-login-mobile]').should('not.be.visible'); 33 | cy.get('[data-testid=navbar-login-desktop]').should('not.be.visible'); 34 | cy.get('[data-testid=navbar-toggle]').should('be.visible'); 35 | cy.get('[data-testid=navbar-toggle]').click(); 36 | cy.get('[data-testid=navbar-items]').should('be.visible'); 37 | cy.get('[data-testid=navbar-login-mobile]').should('be.visible'); 38 | cy.get('[data-testid=navbar-login-desktop]').should('not.be.visible'); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /Sample-01/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | const navbarActiveClass = 'navbar-item-active'; 2 | 3 | Cypress.Commands.add( 4 | 'isActive', 5 | { 6 | prevSubject: true 7 | }, 8 | selector => { 9 | cy.get(selector).should('have.class', navbarActiveClass); 10 | 11 | const selectedItems = cy.get('[data-testid=navbar-items]').find('[data-testid|=navbar]'); 12 | if (selectedItems.length > 1) selectedItems.not(selector).should('not.have.class', navbarActiveClass); 13 | } 14 | ); 15 | 16 | Cypress.Commands.add('mobileViewport', () => cy.viewport(500, 1000)); 17 | -------------------------------------------------------------------------------- /Sample-01/cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | import './commands'; 2 | 3 | Cypress.on('uncaught:exception', err => { 4 | if ( 5 | /hydrat/i.test(err.message) || 6 | /Minified React error #418/.test(err.message) || 7 | /Minified React error #423/.test(err.message) 8 | ) { 9 | return false; 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /Sample-01/exec.ps1: -------------------------------------------------------------------------------- 1 | docker build -t auth0-nextjs-01-login . 2 | docker run --init -p 3000:3000 -p 3001:3001 -it auth0-nextjs-01-login 3 | -------------------------------------------------------------------------------- /Sample-01/exec.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | docker build -t auth0-nextjs-01-login . 3 | docker run --init -p 3000:3000 -p 3001:3001 -it auth0-nextjs-01-login 4 | -------------------------------------------------------------------------------- /Sample-01/lib/auth0.js: -------------------------------------------------------------------------------- 1 | import { Auth0Client } from "@auth0/nextjs-auth0/server"; 2 | 3 | // Initialize the Auth0 client 4 | export const auth0 = new Auth0Client({ 5 | // Options are loaded from environment variables by default 6 | // Ensure necessary environment variables are properly set 7 | // domain: process.env.AUTH0_DOMAIN, 8 | // clientId: process.env.AUTH0_CLIENT_ID, 9 | // clientSecret: process.env.AUTH0_CLIENT_SECRET, 10 | // appBaseUrl: process.env.APP_BASE_URL, 11 | // secret: process.env.AUTH0_SECRET, 12 | authorizationParameters: { 13 | // In v4, the AUTH0_SCOPE and AUTH0_AUDIENCE environment variables are no longer automatically picked up by the SDK. 14 | // Instead, we need to provide the values explicitly. 15 | scope: process.env.AUTH0_SCOPE, 16 | audience: process.env.AUTH0_AUDIENCE, 17 | } 18 | }); -------------------------------------------------------------------------------- /Sample-01/middleware.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { auth0 } from "./lib/auth0" 3 | 4 | export async function middleware(request) { 5 | const authRes = await auth0.middleware(request); 6 | 7 | // authentication routes — let the middleware handle it 8 | if (request.nextUrl.pathname.startsWith("/auth")) { 9 | return authRes; 10 | } 11 | 12 | // public routes — no need to check for session 13 | if (request.nextUrl.pathname === ("/")) { 14 | return authRes; 15 | } 16 | 17 | const { origin } = new URL(request.url) 18 | const session = await auth0.getSession() 19 | 20 | // user does not have a session — redirect to login 21 | if (!session) { 22 | return NextResponse.redirect(`${origin}/auth/login`) 23 | } 24 | 25 | return authRes 26 | } 27 | 28 | export const config = { 29 | matcher: [ 30 | /* 31 | * Match all request paths except for the ones starting with: 32 | * - _next/static (static files) 33 | * - _next/image (image optimization files) 34 | * - favicon.ico, sitemap.xml, robots.txt (metadata files) 35 | * - api (API routes) 36 | */ 37 | "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|api).*)", 38 | ], 39 | } -------------------------------------------------------------------------------- /Sample-01/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | poweredByHeader: false 3 | }; 4 | -------------------------------------------------------------------------------- /Sample-01/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth0-nextjs-sample", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "concurrently \"next dev\" \"node api-server\"", 7 | "dev:api": "nodemon api-server", 8 | "build": "next build", 9 | "start": "concurrently \"next start\" \"node api-server\"", 10 | "start:api": "node api-server", 11 | "test": "vitest run", 12 | "test:watch": "vitest watch", 13 | "test:integration": "start-server-and-test start http-get://localhost:3000 cypress:run", 14 | "test:integration:watch": "start-server-and-test start http-get://localhost:3000 cypress:open", 15 | "cypress:run": "cypress run --browser chrome", 16 | "cypress:open": "cypress open --browser chrome" 17 | }, 18 | "dependencies": { 19 | "@auth0/nextjs-auth0": "^4.2.1", 20 | "@fortawesome/fontawesome-svg-core": "^6.7.2", 21 | "@fortawesome/free-solid-svg-icons": "^6.7.2", 22 | "@fortawesome/react-fontawesome": "^0.2.2", 23 | "concurrently": "^8.2.0", 24 | "cors": "^2.8.5", 25 | "dotenv": "^16.0.3", 26 | "express": "^4.18.2", 27 | "express-jwt": "^8.0", 28 | "helmet": "^7.1.0", 29 | "highlight.js": "^11.9.0", 30 | "jwks-rsa": "^3.1.0", 31 | "morgan": "^1.10.0", 32 | "next": "15.2.4", 33 | "nodemon": "^3.0.0", 34 | "react": "19.0.0", 35 | "react-dom": "19.0.0", 36 | "reactstrap": "^9.1.5" 37 | }, 38 | "devDependencies": { 39 | "@babel/core": "^7.23.0", 40 | "@testing-library/dom": "^10.4.0", 41 | "@testing-library/jest-dom": "^6.6.3", 42 | "@testing-library/react": "^16.2.0", 43 | "@vitejs/plugin-react": "^4.3.4", 44 | "babel-jest": "^29.7.0", 45 | "cypress": "^12.7.0", 46 | "eslint-config-next": "15.2.4", 47 | "identity-obj-proxy": "^3.0.0", 48 | "jest": "^29.7.0", 49 | "jest-environment-jsdom": "^29.7.0", 50 | "prettier": "^3.1.0", 51 | "start-server-and-test": "^2.0.0", 52 | "vitest": "^3.0.9" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sample-01/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0-samples/auth0-nextjs-samples/5a7cde63524aedca786787ec62dd46c43ad258b1/Sample-01/public/favicon.ico -------------------------------------------------------------------------------- /Sample-01/tests/app/api/shows.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | import { vi } from 'vitest'; 5 | import { GET as shows } from '../../../app/api/shows/route'; 6 | 7 | const req = vi.fn(); 8 | const res = (() => { 9 | const mock = {}; 10 | mock.status = vi.fn().mockReturnValue(mock); 11 | mock.json = vi.fn().mockReturnValue(mock); 12 | return mock; 13 | })(); 14 | 15 | describe('/api/shows', () => { 16 | afterAll(() => { 17 | delete global.fetch; 18 | }); 19 | 20 | it('should call the external API', async () => { 21 | global.fetch = vi.fn().mockReturnValue({ json: () => Promise.resolve({ msg: 'Text' }) }); 22 | 23 | const res = await shows(req); 24 | 25 | expect(res.status).toBe(200); 26 | await expect(res.json()).resolves.toEqual({ msg: 'Text' }); 27 | }); 28 | 29 | it('should fail when the external API call fails', async () => { 30 | global.fetch = vi.fn().mockReturnValue({ json: () => Promise.reject(new Error('Error')) }); 31 | 32 | const res = await shows(req); 33 | 34 | expect(res.status).toBe(500); 35 | await expect(res.json()).resolves.toEqual({ error: 'Error' }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /Sample-01/tests/app/csr.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { Auth0Provider } from '@auth0/nextjs-auth0'; 4 | 5 | import CSRPage from '../../app/csr/page'; 6 | 7 | describe('csr', () => { 8 | it('should render without crashing', async () => { 9 | render( 10 | 11 | 12 | 13 | ); 14 | 15 | expect(screen.getByTestId('csr')).toBeInTheDocument(); 16 | expect(screen.getByTestId('csr-title')).toBeInTheDocument(); 17 | expect(screen.getByTestId('csr-text')).toBeInTheDocument(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /Sample-01/tests/app/external.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent, waitFor } from '@testing-library/react'; 3 | 4 | import _External from '../../app/external/page'; 5 | import { Auth0Provider } from '@auth0/nextjs-auth0'; 6 | 7 | const External = () => ( 8 | 9 | <_External /> 10 | 11 | ); 12 | 13 | describe('index', () => { 14 | afterAll(() => { 15 | delete global.fetch; 16 | }); 17 | 18 | it('should render without crashing', async () => { 19 | render(); 20 | 21 | expect(screen.queryByTestId('loading')).not.toBeInTheDocument(); 22 | expect(screen.getByTestId('external')).toBeInTheDocument(); 23 | expect(screen.getByTestId('external-title')).toBeInTheDocument(); 24 | expect(screen.getByTestId('external-text')).toBeInTheDocument(); 25 | expect(screen.getByTestId('external-action')).toBeInTheDocument(); 26 | expect(screen.queryByTestId('external-result')).not.toBeInTheDocument(); 27 | }); 28 | 29 | it('should render a spinner when the button is clicked', async () => { 30 | global.fetch = () => ({ json: () => Promise.resolve() }); 31 | 32 | render(); 33 | 34 | fireEvent.click(screen.getByTestId('external-action')); 35 | 36 | waitFor(() => screen.getByTestId('loading').toBeInTheDocument()); 37 | waitFor(() => screen.queryByTestId('external-result').not.toBeInTheDocument()); 38 | }); 39 | 40 | it('should call the API when the button is clicked', async () => { 41 | global.fetch = () => ({ json: () => Promise.resolve({ msg: 'Text' }) }); 42 | 43 | render(); 44 | 45 | fireEvent.click(screen.getByTestId('external-action')); 46 | await waitFor(() => screen.getByTestId('external-result')); 47 | 48 | expect(screen.queryByTestId('loading')).not.toBeInTheDocument(); 49 | expect(await screen.findByText(/Text/)).toBeInTheDocument(); 50 | }); 51 | 52 | it('should render an error when the API call fails', async () => { 53 | global.fetch = () => ({ json: () => Promise.reject(new Error('Error')) }); 54 | 55 | render(); 56 | 57 | fireEvent.click(screen.getByTestId('external-action')); 58 | await waitFor(() => screen.getByTestId('external-result')); 59 | 60 | expect(screen.queryByTestId('loading')).not.toBeInTheDocument(); 61 | expect(await screen.findByText('Error')).toBeInTheDocument(); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /Sample-01/tests/app/index.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | 4 | import Index from '../../app/page'; 5 | 6 | describe('index', () => { 7 | it('should render without crashing', async () => { 8 | render(); 9 | 10 | expect(screen.getByTestId('hero')).toBeInTheDocument(); 11 | expect(screen.getByTestId('content')).toBeInTheDocument(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /Sample-01/tests/app/profile.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, waitFor } from '@testing-library/react'; 3 | 4 | import { withAuth0Provider, mockUser } from '../fixtures'; 5 | import Profile from '../../app/profile/page'; 6 | 7 | describe('profile', () => { 8 | it('should render without crashing', async () => { 9 | render(, { wrapper: withAuth0Provider({ user: mockUser }) }); 10 | 11 | expect(screen.queryByTestId('loading')).not.toBeInTheDocument(); 12 | expect(screen.getByTestId('profile')).toBeInTheDocument(); 13 | expect(screen.getByTestId('profile-picture')).toBeInTheDocument(); 14 | expect(screen.getByTestId('profile-name')).toBeInTheDocument(); 15 | expect(screen.getByTestId('profile-json')).toBeInTheDocument(); 16 | }); 17 | 18 | it('should render a spinner when the user is loading', async () => { 19 | render(, { wrapper: withAuth0Provider({ user: undefined }) }); 20 | 21 | waitFor(() => screen.getByTestId('loading').toBeInTheDocument()); 22 | waitFor(() => screen.queryByTestId('profile').not.toBeInTheDocument()); 23 | }); 24 | 25 | it('should render the user profile', async () => { 26 | render(, { wrapper: withAuth0Provider({ user: mockUser }) }); 27 | 28 | waitFor(() => screen.queryByTestId('loading').not.toBeInTheDocument()); 29 | Object.keys(mockUser).forEach(key => { 30 | () => screen.getByTestId('profile-json').text().toContain(mockUser[key]); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /Sample-01/tests/app/ssr.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | 4 | import { mockUser } from '../fixtures'; 5 | import SSRPageComponent from '../../app/ssr/page'; 6 | 7 | describe('ssr', () => { 8 | it('should render without crashing', async () => { 9 | const SSRPage = await SSRPageComponent(); 10 | render(SSRPage); 11 | 12 | expect(screen.getByTestId('ssr')).toBeInTheDocument(); 13 | expect(screen.getByTestId('ssr-title')).toBeInTheDocument(); 14 | expect(screen.getByTestId('ssr-text')).toBeInTheDocument(); 15 | expect(screen.getByTestId('ssr-json')).toBeInTheDocument(); 16 | }); 17 | 18 | it('should render the user profile', async () => { 19 | const SSRPage = await SSRPageComponent(); 20 | render(SSRPage); 21 | 22 | Object.keys(mockUser).forEach(key => { 23 | () => screen.getByTestId('ssr-json').text().toContain(mockUser[key]); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /Sample-01/tests/components/AnchorLink.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | 4 | import AnchorLink from '../../components/AnchorLink'; 5 | 6 | describe('AnchorLink', () => { 7 | it('should render without crashing', async () => { 8 | render( 9 | 10 | Text 11 | 12 | ); 13 | 14 | expect(screen.getByTestId('anchor-link')).toBeInTheDocument(); 15 | expect(screen.getByText('Text')).toBeInTheDocument(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /Sample-01/tests/components/Content.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | 4 | import Content from '../../components/Content'; 5 | 6 | describe('Content', () => { 7 | it('should render without crashing', async () => { 8 | render(); 9 | 10 | expect(screen.getByTestId('content')).toBeInTheDocument(); 11 | expect(screen.getByTestId('content-title')).toBeInTheDocument(); 12 | expect(screen.getByTestId('content-items')).toBeInTheDocument(); 13 | expect(screen.getByTestId('content-items').children).toHaveLength(4); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /Sample-01/tests/components/ErrorMessage.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | 4 | import ErrorMessage from '../../components/ErrorMessage'; 5 | 6 | describe('ErrorMessage', () => { 7 | it('should render without crashing', async () => { 8 | const error = new Error('Error'); 9 | 10 | render({error.message}); 11 | 12 | expect(screen.getByTestId('error')).toBeInTheDocument(); 13 | expect(screen.getByText('Error')).toBeInTheDocument(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /Sample-01/tests/components/Footer.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | 4 | import Footer from '../../components/Footer'; 5 | 6 | describe('Footer', () => { 7 | it('should render without crashing', async () => { 8 | render(