diff --git a/README.md b/README.md index 1d405f6..d4d9448 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,254 @@ # Descope SDK for NextJS -Notes: +The Descope SDK for NextJS provides convenient access to the Descope for an application written on top of NextJS. You can read more on the [Descope Website](https://descope.com). -- document sessionTokenViaCookie is default to true +This SDK uses under the hood the Descope React SDK and Descope Node SDK +Refer to the [Descope React SDK](https://github.com/descope/react-sdk) and [Descope Node SDK](https://github.com/descope/node-sdk) for more details. + +## Requirements + +- The SDK supports NextJS version 13 and above. +- A Descope `Project ID` is required for using the SDK. Find it on the [project page in the Descope Console](https://app.descope.com/settings/project). + +## Installing the SDK + +Install the package with: + +```bash +npm i --save @descope/nextjs-sdk +``` + +## Usage + +This section contains guides for App router and Pages router. +For Pages router, see the [Pages Router](#pages-router) section. + +### App Router + +#### Wrap your app layout with Auth Provider + +```js +// src/app/layout.tsx + +import { AuthProvider } from '@descope/nextjs-sdk' + +export default function RootLayout({ + children +}: { + children: React.ReactNode +}) { + return ( + + + {children} + + + ) +} +``` + +Note: `AuthProvider` uses `sessionTokenViaCookie` by default, in order that the [AuthMiddleware](<#Require-authentication-for-application-(Middleware)>) will work out of the box. + +#### Use Descope to render Flow + +You can use **default flows** or **provide flow id** directly to the Descope component + +```js +// Login page, e.g. src/app/sign-in.tsx +import { Descope } from '@descope/react-sdk'; +// you can choose flow to run from the following without `flowId` instead +// import { SignInFlow, SignUpFlow, SignUpOrInFlow } from '@descope/react-sdk' + +const Page = () => { + return ( + console.log('Logged in!')} + onError={(e) => console.log('Could not logged in!')} + redirectAfterSuccess="/" + // redirectAfterError="/error-page" + /> + ); +}; +``` + +Refer to the [Descope React SDK Section](https://github.com/descope/react-sdk?tab=readme-ov-file#2-provide-flow-id) for a list of available props. + +**Note:** Descope is a client component. if the component that renders it is a server component, you cannot pass `onSuccess`/`onError`/`errorTransformer`/`logger` props because they are not serializable. To redirect the user after the flow is completed, use the `redirectAfterSuccess` and `redirectAfterError` props. + +#### Client Side Usage + +Use the `useDescope`, `useSession` and `useUser` hooks in your components in order to get authentication state, user details and utilities + +This can be helpful to implement application-specific logic. Examples: + +- Render different components if current session is authenticated +- Render user's content +- Logout button + +Note: these hooks should be used in a client component only (For example, component with `use client` notation). + +```js +'use client'; +import { useDescope, useSession, useUser } from '@descope/nextjs-sdk/client'; +import { useCallback } from 'react'; + +const App = () => { + // NOTE - `useDescope`, `useSession`, `useUser` should be used inside `AuthProvider` context, + // and will throw an exception if this requirement is not met + // useSession retrieves authentication state, session loading status, and session token + const { isAuthenticated, isSessionLoading, sessionToken } = useSession(); + // useUser retrieves the logged in user information + const { user } = useUser(); + // useDescope retrieves Descope SDK for further operations related to authentication + // such as logout + const sdk = useDescope(); + + if (isSessionLoading || isUserLoading) { + return

Loading...

; + } + + const handleLogout = useCallback(() => { + sdk.logout(); + }, [sdk]); + + if (isAuthenticated) { + return ( + <> +

Hello {user.name}

+ + + ); + } + + return

You are not logged in

; +}; +``` + +#### Server Side Usage + +##### Require authentication for application (Middleware) + +You can use NextJS Middleware to require authentication for a page/route or a group of pages/routes. + +Descope SDK provides a middleware function that can be used to require authentication for a page/route or a group of pages/routes. + +```js +// src/middleware.ts +import { authMiddleware } from '@descope/nextjs-sdk/server' + +export default authMiddleware({ + // The Descope project ID to use for authentication + // Defaults to process.env.DESCOPE_PROJECT_ID + projectId: 'your-descope-project-id' + + // The URL to redirect to if the user is not authenticated + // Defaults to process.env.SIGN_IN_ROUTE or '/sign-in' if not provided + redirectUrl?: string + + // An array of public routes that do not require authentication + // In addition to the default public routes: + // - process.env.SIGN_IN_ROUTE or /sign-in if not provided + // - process.env.SIGN_UP_ROUTE or /sign-up if not provided + publicRoutes?: string[] +}) + +export const config = { + matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'] +} +``` + +##### Read session information in server side + +use the `session()` helper to read session information in Server Components and Route handlers. + +Note: `session()` requires the `authMiddleware` to be used for the Server Component or Route handler that uses it. + +Server Component: + +```js +// src/app/page.tsx + +import { session } from '@descope/nextjs-sdk/server'; + +async function Page() { + const sessionRes = session(); + if (!sessionRes) { + // ... + } + // Use the session jwt or parsed token + const { jwt, token } = sessionRes; +} +``` + +Route handler: + +```js +// src/pages/api/routes.ts +export async function GET() { + const currSession = session(); + if (!currSession.isAuthenticated) { + // ... + } + + // Use the session jwt or parsed token + const { jwt, token } = currSession; +} +``` + +#### Access Descope SDK in server side + +Use `createSdk` function to create Descope SDK in server side. + +Refer to the [Descope Node SDK](https://github.com/descope/node-sdk/?tab=readme-ov-file#authentication-functions) for a list of available functions. + +Usage example in Route handler: + +```js +// src/pages/api/routes.ts +import { createSdk } from '@descope/nextjs-sdk/server'; + +const sdk = createSdk({ + // The Descope project ID to use for authentication + // Defaults to process.env.DESCOPE_PROJECT_ID + projectId: 'your-descope-project-id', + + // The Descope management key to use for management operations + // Defaults to process.env.DESCOPE_MANAGEMENT_KEY + managementKey: 'your-descope-management-key' +}); + +export async function GET(req) { + const { searchParams } = new URL(req.url); + const loginId = searchParams.get('loginId'); + + const { ok, data: user } = await sdk.management.user.load(loginId); + if (!ok) { + // ... + } + // Use the user data ... +} +``` + +### Pages Router + +This section is Working in progress :-) +In the meantime, you can see the example in the [Pages Router](/examples/pages-router/) folder. + +## Code Example + +You can find an example react app in the [examples folder](./examples). - [App Router](/examples/app-router/) - [Pages Router](/examples/pages-router/) + +## Learn More + +To learn more please see the [Descope Documentation and API reference page](https://docs.descope.com/). + +## Contact Us + +If you need help you can email [Descope Support](mailto:support@descope.com) + +## License + +The Descope SDK for React is licensed for use under the terms and conditions of the [MIT license Agreement](./LICENSE). diff --git a/examples/app-router/README.md b/examples/app-router/README.md new file mode 100644 index 0000000..2ae18d6 --- /dev/null +++ b/examples/app-router/README.md @@ -0,0 +1,46 @@ +# App Router Example + +This example demonstrates how to use NextJS Descope SDK in an App Router. + +## Setup + +1. Build the sdk package: + +```bash +(cd ../../ && npm run build) +``` + +2. Install dependencies: + +```bash +npm install +``` + +3. Set environment variables using the `.env` file: + +```bash +NEXT_PUBLIC_DESCOPE_PROJECT_ID= +NEXT_PUBLIC_DESCOPE_FLOW_ID= +DESCOPE_MANAGEMENT_KEY= # Default is sign-up-or-in +# This is an example of a custom route for the sign-in page +# the /login route is the one that this example uses +SIGN_IN_ROUTE="/login" +``` + +## Run the example + +```bash +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +## Usage + +This app has the following parts + +- Layout `src/app/layout.tsx` - a layout that wraps the app layout with the Auth Provider +- Home page `src/app/page.tsx` - a Server Component that renders a Client Component (`UserDetails`) +- Login page `src/app/login.tsx` - a Server Component that renders Descope Flow Component +- Authentication middleware `src/middleware.ts` - a middleware that checks if the user is authenticated and redirects to the login page if not +- Route handler `src/app/api/route.ts` - a route handler that returns the user's details using the Descope Management SDK. use `curl -H "Authorization: Bearer " http://localhost:3000/api` to test it diff --git a/examples/app-router/app/login/page.tsx b/examples/app-router/app/login/page.tsx index a6bf346..7e8f585 100644 --- a/examples/app-router/app/login/page.tsx +++ b/examples/app-router/app/login/page.tsx @@ -14,7 +14,10 @@ export default function Login() {

App Router Login

{/* Note that if the component is rendered on the server you cannot pass onSuccess/onError callbacks because they are not serializable. */} - + ); } diff --git a/examples/pages-router/README.md b/examples/pages-router/README.md new file mode 100644 index 0000000..6beaaa9 --- /dev/null +++ b/examples/pages-router/README.md @@ -0,0 +1,46 @@ +# Pages Router Example + +This example demonstrates how to use NextJS Descope SDK in an Pages Router. + +## Setup + +1. Build the sdk package: + +```bash +(cd ../../ && npm run build) +``` + +2. Install dependencies: + +```bash +npm install +``` + +3. Set environment variables using the `.env` file: + +```bash +NEXT_PUBLIC_DESCOPE_PROJECT_ID= +NEXT_PUBLIC_DESCOPE_FLOW_ID= +DESCOPE_MANAGEMENT_KEY= # Default is sign-up-or-in +# This is an example of a custom route for the sign-in page +# the /login route is the one that this example uses +SIGN_IN_ROUTE="/login" +``` + +## Run the example + +```bash +npm run dev +``` + +Open [http://localhost:3001](http://localhost:3001) with your browser to see the result. + +## Usage + +This app has the following parts + +- Layout `pages/_app.tsx` - a layout that wraps the app layout with the Auth Provider +- Home page `pages/index.tsx` - a component that renders another Component (`UserDetails`) that uses Descope client hooks +- Login page `pages/login.tsx` - a Component that renders Descope Flow Component +- Authentication middleware `src/middleware.ts` - a middleware that checks if the user is authenticated and redirects to the login page if not +- Route handler `src/pages/api/index.ts` - a route handler that returns the user's details using the Descope Management SDK. use `curl -H "Authorization: Bearer " http://localhost:3001/api` to test it diff --git a/examples/pages-router/pages/api/index.ts b/examples/pages-router/pages/api/index.ts index ca15fac..462243a 100644 --- a/examples/pages-router/pages/api/index.ts +++ b/examples/pages-router/pages/api/index.ts @@ -1,5 +1,5 @@ import type { NextApiRequest, NextApiResponse } from 'next'; -import { createSdk, session } from '@descope/nextjs-sdk/server'; +import { createSdk, getSession } from '@descope/nextjs-sdk/server'; const sdk = createSdk({ projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID, @@ -10,10 +10,7 @@ export default async function handler( req: NextApiRequest, res: NextApiResponse ) { - // Temporary workaround for the headers issue in pages router - const currentSession = session( - new Headers(req.headers as { [key: string]: string }) - ); + const currentSession = getSession(req); if (!currentSession) { return res.status(401).json({ message: 'Unauthorized' }); } diff --git a/examples/pages-router/pages/login.tsx b/examples/pages-router/pages/login.tsx index e1c6d03..9ab8473 100644 --- a/examples/pages-router/pages/login.tsx +++ b/examples/pages-router/pages/login.tsx @@ -1,16 +1,16 @@ import { Descope } from '@descope/nextjs-sdk'; import { useSession } from '@descope/nextjs-sdk/client'; -import React, { useState } from 'react'; +import React from 'react'; export default function Login() { - // const [sessionToken, setSessionToken] = useState(null); useSession(); return (

Pages Router Login

- {/* Note that if the component that renders Descope is a server component, - you cannot pass onSuccess/onError callbacks because they are not serializable. */} - +
); } diff --git a/src/server/authMiddleware.ts b/src/server/authMiddleware.ts index ec4a9f0..30c1d33 100644 --- a/src/server/authMiddleware.ts +++ b/src/server/authMiddleware.ts @@ -9,10 +9,6 @@ type MiddlewareOptions = { // Defaults to process.env.DESCOPE_PROJECT_ID projectId?: string; - // The Descope management key to use for authentication - // Defaults to process.env.DESCOPE_MANAGEMENT_KEY - managementKey?: string; - // The URL to redirect to if the user is not authenticated // Defaults to process.env.SIGN_IN_ROUTE or '/sign-in' if not provided redirectUrl?: string; @@ -79,8 +75,7 @@ const createAuthMiddleware = (options: MiddlewareOptions = {}) => { let session: AuthenticationInfo | undefined; try { session = await getGlobalSdk({ - projectId: options.projectId, - managementKey: options.managementKey + projectId: options.projectId }).validateJwt(jwt); } catch (err) { console.debug('Auth middleware, Failed to validate JWT', err); diff --git a/src/server/index.ts b/src/server/index.ts index ed4fee9..c9dbb4a 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,3 +1,3 @@ export { default as authMiddleware } from './authMiddleware'; -export { default as session } from './session'; +export { session, getSession } from './session'; export { createSdk } from './sdk'; diff --git a/src/server/sdk.ts b/src/server/sdk.ts index c634761..41d4b7f 100644 --- a/src/server/sdk.ts +++ b/src/server/sdk.ts @@ -9,7 +9,7 @@ type CreateSdkParams = Omit[0], 'projectId'> & { let globalSdk: Sdk; export const getGlobalSdk = ( - config?: Pick + config?: Pick ): Sdk => { if (!globalSdk) { if (!config?.projectId && !process.env.DESCOPE_PROJECT_ID) { diff --git a/src/server/session.ts b/src/server/session.ts index 9c213c2..50143b2 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1,14 +1,11 @@ import { AuthenticationInfo } from '@descope/node-sdk'; +import { NextApiRequest } from 'next'; import { headers } from 'next/headers'; import { DESCOPE_SESSION_HEADER } from './constants'; -// returns the session token if it exists in the headers -// This function require middleware -export default function session( - requestHeaders?: Headers +function extractSession( + descopeSession?: string ): AuthenticationInfo | undefined { - const readyHeaders = requestHeaders || headers(); - const descopeSession = readyHeaders?.get(DESCOPE_SESSION_HEADER); if (!descopeSession) { return undefined; } @@ -21,3 +18,18 @@ export default function session( return undefined; } } +// returns the session token if it exists in the headers +// This function require middleware +export function session(): AuthenticationInfo | undefined { + return extractSession(headers()?.get(DESCOPE_SESSION_HEADER)); +} + +// returns the session token if it exists in the request headers +// This function require middleware +export function getSession( + req: NextApiRequest +): AuthenticationInfo | undefined { + return extractSession( + req.headers[DESCOPE_SESSION_HEADER.toLowerCase()] as string + ); +}