Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,25 @@ jobs:
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # pin@7.0.0
with:
token: ${{ secrets.CODECOV_TOKEN }}

rsc-directive:
name: Verify 'use client' directive
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Comment thread
gyaneshgouraw-okta marked this conversation as resolved.

- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm

- name: Install dependencies
run: npm ci

- name: Build bundles and assert 'use client' directive
run: npm run test:dist
env:
ASSERT_USE_CLIENT: '1'
69 changes: 69 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- [Protecting a route in a `react-router-dom v6` app](#protecting-a-route-in-a-react-router-dom-v6-app)
- [Protecting a route in a Gatsby app](#protecting-a-route-in-a-gatsby-app)
- [Protecting a route in a Next.js app (in SPA mode)](#protecting-a-route-in-a-nextjs-app-in-spa-mode)
- [Using with the Next.js App Router (Server Components)](#using-with-the-nextjs-app-router-server-components)
- [Use with Auth0 organizations](#use-with-auth0-organizations)
- [Protecting a route with a claims check](#protecting-a-route-with-a-claims-check)
- [Device-bound tokens with DPoP](#device-bound-tokens-with-dpop)
Expand Down Expand Up @@ -460,6 +461,74 @@ export default withAuthenticationRequired(Profile);

See [Next.js example app](./examples/nextjs-app)

## Using with the Next.js App Router (Server Components)

In the Next.js App Router every module is a React Server Component by default. `@auth0/auth0-react` is a client-only library (it relies on React hooks and browser APIs), and its published ESM and CommonJS bundles ship the `'use client'` directive baked in. That means you can import `Auth0Provider` **directly** into a Server Component such as your root `app/layout.tsx`, without writing your own `'use client'` wrapper.

The `redirect_uri` value is evaluated where the JSX is authored — the Server Component — where `window` is not available. So pass an explicit `redirect_uri` from an environment variable (or another value known at build/server time) rather than `window.location.origin`:

```tsx
// app/layout.tsx (Server Component, no 'use client' directive needed)
import { Auth0Provider } from '@auth0/auth0-react';

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Auth0Provider
domain={process.env.NEXT_PUBLIC_AUTH0_DOMAIN as string}
clientId={process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID as string}
authorizationParams={{
redirect_uri: process.env.NEXT_PUBLIC_BASE_URL as string,
}}
>
{children}
</Auth0Provider>
</body>
</html>
);
}
```

Any component that **calls** a hook such as `useAuth0()` runs on the client, so that component must be a Client Component. Because `useAuth0()` reads the client-side Auth0 context, the component that calls it must be marked with `'use client'`.

```tsx
// app/page.tsx
'use client';

import { useAuth0 } from '@auth0/auth0-react';

export default function Home() {
const { isAuthenticated, isLoading, loginWithRedirect, logout, user } =
useAuth0();

if (isLoading) return <p>Loading...</p>;

return isAuthenticated ? (
<>
<p>Signed in as {user?.name}</p>
<button
onClick={() =>
logout({ logoutParams: { returnTo: window.location.origin } })
}
>
Log out
</button>
</>
) : (
<button onClick={() => loginWithRedirect()}>Log in</button>
);
}
```

Reading `window.location.origin` is safe here precisely because `app/page.tsx` is a Client Component (marked with `'use client'`), so `window` exists at runtime — unlike in the Server Component above.

In short: the provider can live in a Server Component, while components that consume the hooks opt into the client, exactly as with any other client-only React library.

## Use with Auth0 organizations

[Organizations](https://auth0.com/docs/organizations) is a set of features that provide better support for developers who build and maintain SaaS and Business-to-Business (B2B) applications. Note that Organizations is currently only available to customers on our Enterprise and Startup subscription plans.
Expand Down
79 changes: 79 additions & 0 deletions __tests__/use-client-directive.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* @jest-environment node
*
* React Server Components support.
*
* These assertions run against the BUILT bundles in dist/, not src/. The entry
* (src/index.tsx) is a re-export barrel with no directive of its own, and the
* 'use client' directives in the imported leaf modules are dropped when Rollup
* concatenates them into a single file, so the directive is injected at the
* bundle top via rollup's output.banner instead. The RSC-consumed entry points
* are the CJS (main) and ESM (module) outputs; the UMD <script> builds
* intentionally do NOT carry it.
*
* dist/ is not built by `npm test` (which is just `jest --coverage`), so when the
* bundles are absent these tests skip with a clear message. Run `npm run test:dist`
* to build first and assert against fresh output. The dedicated CI job sets
* ASSERT_USE_CLIENT, under which missing bundles are a hard error rather than a
* silent skip, so the guarantee can never masquerade as a pass. Files are read as
* TEXT (never imported), so nothing here is pulled into coverage collection.
*/
import { existsSync, readFileSync } from 'fs';
import { resolve } from 'path';

const dist = (file: string): string => resolve(__dirname, '..', 'dist', file);

const firstNonEmptyLine = (file: string): string => {
const line = readFileSync(file, 'utf8')
.split('\n')
.map((l) => l.trim())
.find((l) => l.length > 0);
if (line === undefined) {
throw new Error(
`Bundle ${file} has no non-empty lines (empty or truncated build output?)`
);
}
return line;
};

// A 'use client' directive is a bare string-literal statement, e.g. `'use client';`
// on its own line. It must NOT be confused with the same text appearing inside a
// JSDoc comment (the Auth0Provider doc block mentions the directive, and that prose
// is carried into every bundle). This matches a standalone directive only.
const hasClientDirective = (file: string): boolean =>
readFileSync(file, 'utf8')
.split('\n')
.map((l) => l.trim())
.some((l) => l === "'use client';" || l === '"use client";');

const rscBundles = ['auth0-react.cjs.js', 'auth0-react.esm.js'];
const umdBundles = ['auth0-react.js', 'auth0-react.min.js'];

const distBuilt = [...rscBundles, ...umdBundles].every((f) => existsSync(dist(f)));

if (!distBuilt) {
// The dedicated CI job sets ASSERT_USE_CLIENT (after building the bundles), so a
// missing dist/ there is a hard error (the guarantee can never silently pass). Any
// other run (e.g. `npm test`, which doesn't build) just skips.
if (process.env.ASSERT_USE_CLIENT) {
throw new Error(
"dist/ bundles are missing but the 'use client' directive must be asserted. " +
'Run `npm run test:dist`, which builds the bundles before running this suite.'
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
console.warn(
'Skipping use-client-directive tests: dist/ not built. Run `npm run test:dist`.'
);
}

const describeIfBuilt = distBuilt ? describe : describe.skip;

describeIfBuilt("'use client' directive in built bundles", () => {
it.each(rscBundles)('%s starts with the client directive', (file) => {
expect(firstNonEmptyLine(dist(file))).toBe("'use client';");
});

it.each(umdBundles)('%s does not carry the client directive', (file) => {
expect(hasClientDirective(dist(file))).toBe(false);
});
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"lint": "eslint --ext=tsx ./src ./__tests__",
"start": "rollup -cw",
"test": "jest --coverage",
"test:dist": "npm run build && jest use-client-directive",
"prepack": "npm run build",
"docs": "typedoc --options typedoc.js src",
"install:examples": "npm i --prefix=examples/cra-react-router --no-package-lock --legacy-peer-deps && npm i --prefix=examples/gatsby-app --no-package-lock --legacy-peer-deps && npm i --prefix=examples/nextjs-app --no-package-lock --legacy-peer-deps && npm ci --prefix=examples/users-api",
Expand Down
7 changes: 7 additions & 0 deletions rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import { createApp } from './scripts/oidc-provider.mjs';
const isProduction = process.env.NODE_ENV === 'production';
const name = 'reactAuth0';
const input = 'src/index.tsx';
// Mark the bundle as a client module so it can be imported into a React Server
// Component graph (Next.js App Router). Rollup drops the 'use client' directives
// from the source modules when bundling, so we re-add it at the bundle top via
// output.banner. Applied only to the RSC-consumed CJS + ESM outputs (not UMD).
const useClientBanner = "'use client';";
const globals = {
react: 'React',
'react-dom': 'ReactDOM',
Expand Down Expand Up @@ -79,6 +84,7 @@ export default [
file: pkg.main,
format: 'cjs',
sourcemap: true,
banner: useClientBanner,
},
plugins,
},
Expand All @@ -88,6 +94,7 @@ export default [
file: pkg.module,
format: 'esm',
sourcemap: true,
banner: useClientBanner,
},
plugins,
},
Expand Down
5 changes: 5 additions & 0 deletions src/auth0-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ const defaultOnRedirectCallback = (appState?: AppState): void => {
* ```
*
* Provides the Auth0Context to its child components.
*
* The SDK is a client-only module (its published ESM and CommonJS bundles ship the
* `'use client'` directive), so in a Next.js App Router app you can import and render
* `Auth0Provider` directly from a Server Component such as `app/layout.tsx`.
* Components that call hooks like `useAuth0` must still be Client Components.
*/
const Auth0Provider = <TUser extends User = User>(opts: Auth0ProviderOptions<TUser>) => {
const {
Expand Down
Loading