├── .eslintrc.json
├── app
├── favicon.ico
├── api
│ └── audit
│ │ └── route.tsx
├── page.tsx
├── Descope.tsx
├── dashboard
│ ├── logout-button.tsx
│ └── page.tsx
├── globals.css
├── signup
│ └── page.tsx
├── login
│ └── page.tsx
├── AuthProvider.tsx
└── layout.tsx
├── next.config.js
├── postcss.config.js
├── auth.config.ts
├── lib
├── descopeClient.ts
└── helpers.ts
├── .gitignore
├── tailwind.config.ts
├── public
├── vercel.svg
└── next.svg
├── tsconfig.json
├── components
└── layout
│ └── nav.tsx
├── package.json
├── LICENSE
├── middleware.ts
├── README.md
└── descope_middleware
├── helpers.ts
├── archive.ts
└── index.ts
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/descope-sample-apps/next-app-router-sample-app/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {}
3 |
4 | module.exports = nextConfig
5 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/auth.config.ts:
--------------------------------------------------------------------------------
1 | import { DescopeMiddlewareConfig } from "./descope_middleware";
2 |
3 |
4 | export const authConfig = {
5 |
6 | } satisfies DescopeMiddlewareConfig;
7 |
--------------------------------------------------------------------------------
/app/api/audit/route.tsx:
--------------------------------------------------------------------------------
1 | export async function POST(request: Request) {
2 | const body = await request.json();
3 | console.log(body)
4 | return new Response(JSON.stringify(body), {
5 | headers: { 'content-type': 'application/json' },
6 | });
7 | }
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 |
3 | export default function Home() {
4 | return (
5 |
6 |
7 | Welcome to Descope's NextJS Sample App!
8 |
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/app/Descope.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | // TODO: Check why we can't just import Descope
4 | // from '@descope/react-sdk' and directly export it
5 |
6 | import dynamic from 'next/dynamic';
7 | const Descope = dynamic(
8 | () => import('@descope/react-sdk').then(module => module.Descope),
9 | { ssr: false }
10 | );
11 |
12 | export default Descope;
--------------------------------------------------------------------------------
/lib/descopeClient.ts:
--------------------------------------------------------------------------------
1 | import DescopeClient from '@descope/node-sdk';
2 |
3 | if (!process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID) {
4 | throw new Error('NEXT_PUBLIC_DESCOPE_PROJECT_ID is not defined');
5 | }
6 |
7 |
8 |
9 | const descope = DescopeClient({ projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID, managementKey: process.env.DESCOPE_MANAGEMENT_KEY });
10 | export default descope;
--------------------------------------------------------------------------------
/app/dashboard/logout-button.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useDescope } from "@descope/react-sdk";
4 |
5 | export default function LogoutButton() {
6 | const { logout } = useDescope();
7 | const onLogout = async () => {
8 | await logout();
9 | window.location.href = '/';
10 | }
11 | return (
12 |
13 | )
14 | }
--------------------------------------------------------------------------------
/.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 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 |
3 | const config: Config = {
4 | content: [
5 | './pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './components/**/*.{js,ts,jsx,tsx,mdx}',
7 | './app/**/*.{js,ts,jsx,tsx,mdx}',
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
13 | 'gradient-conic':
14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
15 | },
16 | },
17 | },
18 | plugins: [],
19 | }
20 | export default config
21 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | /* :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | @media (prefers-color-scheme: dark) {
12 | :root {
13 | --foreground-rgb: 255, 255, 255;
14 | --background-start-rgb: 0, 0, 0;
15 | --background-end-rgb: 0, 0, 0;
16 | }
17 | }
18 |
19 | body {
20 | color: rgb(var(--foreground-rgb));
21 | background: linear-gradient(
22 | to bottom,
23 | transparent,
24 | rgb(var(--background-end-rgb))
25 | )
26 | rgb(var(--background-start-rgb));
27 | } */
28 |
--------------------------------------------------------------------------------
/lib/helpers.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import { getSessionToken } from "@/descope_middleware/helpers";
4 | import descope from "./descopeClient";
5 |
6 | const getServerSession = async () => {
7 | const sessionToken = getSessionToken();
8 | const session = await descope.validateSession(sessionToken!);
9 | return session;
10 | }
11 |
12 | const getServerSessionUser = async () => {
13 | const session = await getServerSession();
14 | const userId = session.token.sub;
15 | const userRes = await descope.management.user.loadByUserId(userId!);
16 | const user = userRes.data;
17 | return user;
18 | }
19 |
20 | export { getServerSession, getServerSessionUser };
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/app/signup/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useEffect } from 'react';
4 | import Descope from '../Descope';
5 | import { useSession, useUser } from '@descope/react-sdk';
6 |
7 | export default function Page() {
8 |
9 | const { isAuthenticated, isSessionLoading } = useSession();
10 | const { isUserLoading } = useUser();
11 | // Redirect to login if not authenticated
12 | useEffect(() => {
13 | if (!(isSessionLoading || isUserLoading) && isAuthenticated) {
14 | window.location.href = '/dashboard';
15 | }
16 | });
17 |
18 | return (
19 |
20 |
23 |
24 | )
25 | }
--------------------------------------------------------------------------------
/app/login/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useEffect } from 'react';
4 | import Descope from '../Descope';
5 | import { useDescope, useSession, useUser } from "@descope/react-sdk"
6 |
7 | export default function Page() {
8 |
9 | const { isAuthenticated, isSessionLoading } = useSession();
10 | const { isUserLoading } = useUser();
11 |
12 | // Redirect to login if not authenticated
13 | useEffect(() => {
14 | if (!(isSessionLoading || isUserLoading) && isAuthenticated) {
15 | window.location.href = '/dashboard';
16 | }
17 | });
18 |
19 | return (
20 |
21 |
24 |
25 | )
26 | }
--------------------------------------------------------------------------------
/components/layout/nav.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | export default function Nav() {
4 | return
7 |
8 |
9 |
Descope
10 |
11 |
12 | Log in
13 |
17 | Sign up
18 |
19 |
20 |
21 |
22 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@descope/node-sdk": "^1.6.1",
13 | "@descope/react-sdk": "^2.0.3",
14 | "@descope/web-js-sdk": "^1.7.2",
15 | "jose": "^5.1.3",
16 | "next": "14.0.3",
17 | "react": "^18",
18 | "react-dom": "^18"
19 | },
20 | "devDependencies": {
21 | "@types/node": "^20",
22 | "@types/react": "^18",
23 | "@types/react-dom": "^18",
24 | "autoprefixer": "^10.0.1",
25 | "eslint": "^8",
26 | "eslint-config-next": "14.0.3",
27 | "postcss": "^8",
28 | "tailwindcss": "^3.3.0",
29 | "typescript": "^5"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/AuthProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { AuthProvider as ReactAuthProvider } from '@descope/react-sdk';
4 | import { ReactElement } from 'react';
5 |
6 | interface AuthProviderProps {
7 | children: ReactElement;
8 | projectId: string;
9 | baseUrl?: string;
10 | }
11 |
12 | const AuthProvider = ({ children, projectId, baseUrl }: AuthProviderProps ) => {
13 | return (
14 |
22 | {children}
23 |
24 | );
25 | }
26 |
27 | export default AuthProvider;
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Descope Sample Apps
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 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next'
2 | import { Inter } from 'next/font/google'
3 | import './globals.css'
4 | import AuthProvider from './AuthProvider';
5 | import Nav from '@/components/layout/nav';
6 |
7 | const inter = Inter({ subsets: ['latin'] })
8 |
9 | export const metadata: Metadata = {
10 | title: 'Create Next App',
11 | description: 'Generated by create next app',
12 | }
13 |
14 | export default function RootLayout({
15 | children,
16 | }: {
17 | children: React.ReactNode
18 | }) {
19 |
20 | if (!process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID) {
21 | throw new Error('NEXT_PUBLIC_DESCOPE_PROJECT_ID is not defined');
22 | }
23 |
24 | return (
25 |
29 |
30 |
31 |
32 |
33 | {children}
34 |
35 |
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | // import DescopeMiddleware from "./descope_middleware";
2 | // import { authConfig } from "./auth.config";
3 |
4 | import { NextRequest, NextResponse } from "next/server";
5 | import { getSessionToken, validateSessionToken } from "./descope_middleware/helpers";
6 |
7 |
8 | export async function middleware(request: NextRequest) {
9 | if (request.nextUrl.pathname.startsWith('/dashboard')) {
10 | const sessionJwt = getSessionToken();
11 | const isValid = await validateSessionToken(sessionJwt);
12 | if (!isValid) {
13 | return NextResponse.redirect(new URL('/login', request.url))
14 | }
15 | }
16 |
17 | if (request.nextUrl.pathname === '/') {
18 | const sessionJwt = getSessionToken();
19 | const isValid = await validateSessionToken(sessionJwt);
20 | if (isValid) {
21 | return NextResponse.redirect(new URL('/dashboard', request.url))
22 | }
23 | }
24 |
25 | return NextResponse.next()
26 | }
27 |
28 | // export default DescopeMiddleware(authConfig);
29 |
30 | export const config = {
31 | matcher: [
32 | /*
33 | * Match all request paths except for the ones starting with:
34 | * - api (API routes)
35 | * - _next/static (static files)
36 | * - _next/image (image optimization files)
37 | * - favicon.ico (favicon file)
38 | */
39 | '/((?!api|_next/static|_next/image|favicon.ico).*)',
40 | ],
41 | }
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/app/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | import LogoutButton from "./logout-button";
2 | import { getServerSession, getServerSessionUser } from "@/lib/helpers";
3 |
4 | export default async function Page() {
5 | const session = await getServerSession();
6 | const user = await getServerSessionUser();
7 |
8 | if (!session) {
9 | return No session found
;
10 | }
11 |
12 | if (user) {
13 | return <>
14 | Hello {user.name}
15 |
16 | >;
17 |
18 | } else {
19 | return Error getting user
;
20 | }
21 | }
22 |
23 |
24 |
25 | // 'use client'
26 |
27 | // import { useDescope, useSession, useUser } from "@descope/react-sdk"
28 | // import { useCallback } from "react";
29 | // import { useRouter } from 'next/navigation'
30 | // import LogoutButton from "./logout-button";
31 |
32 | // export default function Page() {
33 |
34 | // const { isAuthenticated, isSessionLoading, sessionToken } = useSession();
35 | // const { user, isUserLoading } = useUser();
36 | // const { logout } = useDescope();
37 | // const router = useRouter()
38 | // const onLogout = async () => {
39 | // await logout();
40 | // router.push('/');
41 | // }
42 |
43 | // if (isSessionLoading || isUserLoading) {
44 | // return Loading...
;
45 | // }
46 |
47 | // if (isAuthenticated) {
48 | // return (
49 | // <>
50 | // Hello {user.name}
51 | //
52 | // >
53 | // );
54 | // }
55 |
56 | // return You are not logged in
;
57 | // }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
37 |
--------------------------------------------------------------------------------
/descope_middleware/helpers.ts:
--------------------------------------------------------------------------------
1 |
2 | import { cookies } from "next/headers";
3 |
4 | if (!process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID) {
5 | throw Error('NEXT_PUBLIC_DESCOPE_PROJECT_ID is required');
6 | }
7 |
8 | const projectId = process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID;
9 |
10 | /**
11 | * Validate an active session
12 | * @param sessionToken session JWT to validate
13 | * @returns AuthenticationInfo promise or throws Error if there is an issue with JWTs
14 | */
15 | export async function validateSessionToken(sessionToken: string | undefined): Promise {
16 | try {
17 | const res = await fetch('https://api.descope.com/v1/auth/validate', {
18 | method: 'POST',
19 | headers: {
20 | 'Authorization': `Bearer ${projectId}:${sessionToken}`,
21 | 'Content-Type': 'application/json',
22 | }
23 | })
24 |
25 | const data = await res.json()
26 |
27 | if (data.errorCode) {
28 | // Handle the error (e.g., invalid token)
29 | console.error('Error:', data.errorMessage);
30 | return false;
31 | } else {
32 | // If there is no error code, the session is valid
33 | // return { isValid: true, jwtParsed: data };
34 | return true;
35 | }
36 | } catch (error) {
37 | throw Error(`session validation failed. Error: ${error}`);
38 | }
39 | };
40 |
41 | // The /v1/auth/validate endpoint
42 | // Returns error object if there is an error
43 | // {
44 | // errorCode: 'E061005',
45 | // errorDescription: 'Invalid token',
46 | // errorMessage: 'Failed to validate invalid JWT for any token - onetime',
47 | // message: 'Failed to validate invalid JWT for any token - onetime'
48 | // }
49 |
50 | // Returns JWT object if there is no error
51 | // {
52 | // "amr": [
53 | // "oauth"
54 | // ],
55 | // "drn": "DS",
56 | // "exp": "2023-12-13T01:07:21Z",
57 | // "iat": "2023-12-13T00:57:21Z",
58 | // "iss": "P2YuRTmSv8PdoasfasfWiLoBrZP92as", // Project ID
59 | // "permissions": [
60 | // "Impersonate",
61 | // "User Admin",
62 | // "SSO Admin"
63 | // ],
64 | // "rexp": "2024-01-10T00:57:21Z",
65 | // "roles": [
66 | // "Tenant Admin"
67 | // ],
68 | // "sub": "U2YualoWDiRkFAEpoyOwAEyT07jkjnj" // User ID
69 | // }
70 |
71 |
72 | /**
73 | * Gets the session token from cookies
74 | * @returns the session token
75 | */
76 | export const getSessionToken = () => {
77 | const cookieStore = cookies()
78 | const sessionJwt = cookieStore.get("DS")?.value;
79 | return sessionJwt;
80 | }
--------------------------------------------------------------------------------
/descope_middleware/archive.ts:
--------------------------------------------------------------------------------
1 | import { JWK, JWTHeaderParameters, KeyLike, errors, importJWK, jwtVerify } from "jose";
2 | import descopeSdk from '@descope/web-js-sdk';
3 |
4 | if (!process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID) {
5 | throw Error('NEXT_PUBLIC_DESCOPE_PROJECT_ID is required');
6 | }
7 |
8 | const projectId = process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID;
9 |
10 |
11 | const sdk = descopeSdk({ projectId });
12 |
13 |
14 | const keys: Record = {};
15 |
16 | /** Get the key that can validate the given JWT KID in the header. Can retrieve the public key from local cache or from Descope. */
17 | async function getKey(header: JWTHeaderParameters): Promise {
18 | if (!header?.kid) throw Error('header.kid must not be empty');
19 |
20 | if (keys[header.kid]) return keys[header.kid];
21 |
22 | // do we need to fetch once or every time?
23 | Object.assign(keys, await fetchKeys());
24 |
25 | if (!keys[header.kid]) {
26 | throw Error('failed to fetch matching key');
27 | }
28 |
29 | return keys[header.kid];
30 | };
31 |
32 |
33 | /** Fetch the public keys (JWKs) from Descope for the configured project */
34 | const fetchKeys = async () => {
35 | const keysWrapper = await sdk.httpClient
36 | .get(`v2/keys/${projectId}`)
37 | .then((resp) => resp.json());
38 | const publicKeys: JWK[] = keysWrapper.keys;
39 | if (!Array.isArray(publicKeys)) return {};
40 | const kidJwksPairs = await Promise.all(
41 | publicKeys.map(async (key) => [key.kid, await importJWK(key)]),
42 | );
43 |
44 | return kidJwksPairs.reduce(
45 | (acc, [kid, jwk]) => (kid ? { ...acc, [kid.toString()]: jwk } : acc),
46 | {},
47 | );
48 | };
49 |
50 |
51 | /**
52 | * Validate the given JWT with the right key and make sure the issuer is correct
53 | * @param jwt the JWT string to parse and validate
54 | * @returns AuthenticationInfo with the parsed token and JWT. Will throw an error if validation fails.
55 | */
56 | async function validateJwt(jwt: string): Promise {
57 | // Do not hard-code the algo because library does not support `None` so all are valid
58 | const res = await jwtVerify(jwt, getKey, { clockTolerance: 5 });
59 | const token = res.payload;
60 |
61 | if (token) {
62 | token.iss = token.iss?.split('/').pop(); // support both url and project id as issuer
63 | if (token.iss !== projectId) {
64 | // We must do the verification here, since issuer can be either project ID or URL
65 | throw new errors.JWTClaimValidationFailed(
66 | 'unexpected "iss" claim value',
67 | 'iss',
68 | 'check_failed',
69 | );
70 | }
71 | }
72 |
73 | return { jwt, token };
74 | };
75 |
--------------------------------------------------------------------------------
/descope_middleware/index.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import type { NextMiddleware, NextRequest } from 'next/server'
3 | import { validateSessionToken, getSessionToken } from './helpers';
4 |
5 | const DEFAULT_IGNORED_ROUTES = [`/((?!api|trpc))(_next.*|.+\\.[\\w]+$)`]
6 | const DEFAULT_API_ROUTES = ['/api/(.*)', '/trpc/(.*)']
7 | const DEFAULT_PUBLIC_ROUTES = ['/', '/login*', '/signup*']
8 | const DEFAULT_UNAUTHENTICATED_URL = '/login';
9 | const DEFAULT_AUTHENTICATED_URL = '/dashboard';
10 |
11 | const includesRoute = (route: string, routes: string[]) => {
12 | return routes.find((x) =>
13 | route.match(new RegExp(`^${x}$`.replace("*$", "($|/)")))
14 | );
15 | }
16 |
17 | export interface DescopeMiddlewareConfig {
18 | publicRoutes?: Array;
19 | ignoredRoutes?: Array;
20 | apiRoutes?: Array;
21 | redirectUrl?: string;
22 | debug?: boolean;
23 | }
24 |
25 | export default function DescopeMiddleware(params?: DescopeMiddlewareConfig): NextMiddleware {
26 | return async function(req: NextRequest) {
27 | const { publicRoutes, ignoredRoutes, apiRoutes, debug } = params || {};
28 |
29 | const pathName = req.nextUrl.pathname;
30 | const isPublicRoute = includesRoute(pathName, publicRoutes || DEFAULT_PUBLIC_ROUTES)
31 | const isIgnoredRoute = includesRoute(pathName, ignoredRoutes || DEFAULT_IGNORED_ROUTES)
32 | const isApiRoute = includesRoute(pathName, apiRoutes || DEFAULT_API_ROUTES)
33 |
34 | // For public routes, ignore
35 | if (isPublicRoute || isIgnoredRoute) {
36 | return NextResponse.next();
37 | }
38 |
39 | const sessionJwt = getSessionToken();
40 |
41 | // For API routes, check for auth header
42 | if (isApiRoute && !sessionJwt) {
43 | return new Response(JSON.stringify({ error: "Unauthorized (Invalid Token)!" }), {
44 | status: 401,
45 | headers: {
46 | 'Content-Type': 'application/json'
47 | }
48 | });
49 | }
50 |
51 | if (!sessionJwt) {
52 | return NextResponse.redirect(new URL(DEFAULT_UNAUTHENTICATED_URL, req.url))
53 | }
54 |
55 | const isSessionValid = await validateSessionToken(sessionJwt);
56 |
57 | if (!isSessionValid) {
58 | if (isApiRoute) {
59 | return new Response(JSON.stringify({ error: "Unauthorized (Invalid Token)!" }), {
60 | status: 401,
61 | headers: {
62 | 'Content-Type': 'application/json'
63 | }
64 | });
65 | }
66 | return NextResponse.redirect(new URL(DEFAULT_UNAUTHENTICATED_URL, req.url))
67 | }
68 |
69 | return NextResponse.next();
70 | }
71 | }
72 |
--------------------------------------------------------------------------------