Skip to content
Merged
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
36 changes: 20 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ code never enters the server bundle.
| --------------------------------------------------------- | -------------------------------------------------------------------- |
| `@auth0/auth0-react-router` (root, resolves to `/client`) | Provider, hooks, UI components, route guards |
| `@auth0/auth0-react-router/server` | `Auth0Server` class, handlers, session and token helpers, middleware |
| `@auth0/auth0-react-router/routes` | Pre-built route config objects for the full OIDC flow |
| `@auth0/auth0-react-router/errors` | Typed error classes |
| `@auth0/auth0-react-router/types` | TypeScript types |
| `@auth0/auth0-react-router/testing` | Test utilities, mock factories, test provider |
Expand All @@ -67,8 +66,8 @@ threading the instance through every call site.

- Login, callback, and logout handled server-side with an encrypted JWE session cookie.
- Back-channel logout so Auth0 can end a user's session from the Dashboard or another app.
- Pre-built route helpers (`auth0Routes`, `handleAuth`) to register the full OIDC flow in one line,
or individual handlers (`handleLogin`, `handleCallback`, `handleLogout`) for custom paths.
- `handleAuth` on a splat route registers the full OIDC flow from a single file, or use the
individual handlers (`handleLogin`, `handleCallback`, `handleLogout`) for custom paths.

**Route protection**

Expand Down Expand Up @@ -129,26 +128,31 @@ the constructor.

**2. Register the auth routes:**

Create a splat route file that handles all `/auth/*` paths:

```tsx
// app/routes/auth.$.tsx
import { handleAuth } from '@auth0/auth0-react-router/server';
import { auth0 } from '../auth0.server';

export const loader = ({ request }) => handleAuth(auth0, request);
export const action = ({ request }) => handleAuth(auth0, request);
```

Register it in your route config:

```ts
// app/routes.ts
import { auth0Routes } from '@auth0/auth0-react-router/routes';
import { auth0 } from './auth0.server';
import { route } from '@react-router/dev/routes';

export default [
{
id: 'root',
path: '/',
// ...
children: [
...auth0Routes(auth0) // registers /auth/login, /auth/callback, /auth/logout
]
}
route('auth/*', 'routes/auth.$.tsx'),
// ...
];
```

`auth0Routes` is the quickest option. If you need custom paths or logic, use the individual
handlers (`handleLogin`, `handleCallback`, `handleLogout`) directly in your own route files, or use
`handleAuth` on a splat route to dispatch all three from one file.
`handleAuth` dispatches internally to `handleLogin`, `handleCallback`, `handleLogout`, and
`handleBackchannelLogout` based on the URL path and HTTP method.

**3. Add the provider to the root layout:**

Expand Down
114 changes: 1 addition & 113 deletions __tests__/server/handlers.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi } from 'vitest';
import {
MissingTransactionError,
BackchannelLogoutError
Expand All @@ -9,7 +9,6 @@ import {
handleLogout,
handleBackchannelLogout,
handleAuth,
auth0Routes,
stripIdTokenClaims
} from '../../src/server/handlers.js';
import { CallbackError } from '../../src/errors/index.js';
Expand Down Expand Up @@ -698,117 +697,6 @@ describe('handleBackchannelLogout', () => {
});
});

// ─── auth0Routes ──────────────────────────────────────────────────────────────

describe('auth0Routes', () => {
let auth0: Auth0Server;

beforeEach(() => {
auth0 = makeAuth0();
});

it('returns an array of 4 route objects', () => {
const routes = auth0Routes(auth0);
expect(routes).toHaveLength(4);
});

it('registers /auth/login with a loader', () => {
const routes = auth0Routes(auth0);
const login = routes.find(r => r.path === '/auth/login');
expect(login).toBeDefined();
expect(login?.loader).toBeTypeOf('function');
expect(login?.action).toBeUndefined();
});

it('registers /auth/callback with a loader', () => {
const routes = auth0Routes(auth0);
const callback = routes.find(r => r.path === '/auth/callback');
expect(callback).toBeDefined();
expect(callback?.loader).toBeTypeOf('function');
expect(callback?.action).toBeUndefined();
});

it('registers /auth/logout with both an action and a loader', () => {
const routes = auth0Routes(auth0);
const logout = routes.find(r => r.path === '/auth/logout');
expect(logout).toBeDefined();
expect(logout?.action).toBeTypeOf('function');
expect(logout?.loader).toBeTypeOf('function');
});

it('registers /auth/backchannel-logout with both an action and a loader', () => {
const routes = auth0Routes(auth0);
const backchannelLogout = routes.find(
r => r.path === '/auth/backchannel-logout'
);
expect(backchannelLogout).toBeDefined();
expect(backchannelLogout?.action).toBeTypeOf('function');
expect(backchannelLogout?.loader).toBeTypeOf('function');
});

it('login loader delegates to handleLogin', async () => {
const startInteractiveLogin = vi
.fn()
.mockResolvedValue(new URL('https://test.auth0.com/authorize'));
auth0 = makeAuth0({ startInteractiveLogin });

const routes = auth0Routes(auth0);
const login = routes.find(r => r.path === '/auth/login')!;

const response = await login.loader!({
request: makeRequest('http://localhost:3000/auth/login')
});

expect(startInteractiveLogin).toHaveBeenCalled();
expect(response.status).toBe(302);
});

it('callback loader delegates to handleCallback', async () => {
const completeInteractiveLogin = vi
.fn()
.mockResolvedValue({ appState: { returnTo: '/home' } });
auth0 = makeAuth0({ completeInteractiveLogin });

const routes = auth0Routes(auth0);
const callback = routes.find(r => r.path === '/auth/callback')!;

const response = await callback.loader!({
request: makeRequest(
'http://localhost:3000/auth/callback?code=abc&state=xyz'
)
});

expect(completeInteractiveLogin).toHaveBeenCalled();
expect(response.status).toBe(302);
expect(response.headers.get('Location')).toBe('/home');
});

it('logout loader returns 405 for GET', async () => {
const routes = auth0Routes(auth0);
const logout = routes.find(r => r.path === '/auth/logout')!;

const response = await logout.loader!({
request: makeRequest('http://localhost:3000/auth/logout')
});

expect(response.status).toBe(405);
expect(response.headers.get('Allow')).toBe('POST');
});

it('logout action rejects GET via handleLogout', async () => {
const routes = auth0Routes(auth0);
const logout = routes.find(r => r.path === '/auth/logout')!;

const response = await logout.action!({
request: makeRequest('http://localhost:3000/auth/logout', {
method: 'GET'
})
});

expect(response.status).toBe(405);
});
});

// ─── handleAuth ───────────────────────────────────────────────────────────────

describe('handleAuth', () => {
Expand Down
6 changes: 0 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,6 @@
"import": "./dist/server/index.js",
"require": "./dist/server/index.cjs"
},
"./routes": {
"browser": null,
"types": "./dist/routes/index.d.ts",
"import": "./dist/routes/index.js",
"require": "./dist/routes/index.cjs"
},
"./errors": {
"types": "./dist/errors/index.d.ts",
"import": "./dist/errors/index.js",
Expand Down
24 changes: 23 additions & 1 deletion src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,23 @@
export { auth0Routes } from '../server/handlers.js';
// auth0Routes() has been removed.
Comment thread
Piyush-85 marked this conversation as resolved.
Comment thread
Piyush-85 marked this conversation as resolved.
//
// React Router's routes.ts only accepts file-based route configs ({ path, file }).
// Inline route objects with loader/action functions are rejected at build time.
//
// To register the Auth0 auth endpoints, create a splat route in your app:
//
// app/routes/auth.$.tsx
// ─────────────────────
// import { Auth0Server, handleAuth } from '@auth0/auth0-react-router/server';
//
// const auth0 = new Auth0Server();
//
// export const loader = ({ request }) => handleAuth(auth0, request);
// export const action = ({ request }) => handleAuth(auth0, request);
//
// app/routes.ts
// ─────────────
// import { route } from '@react-router/dev/routes';
// export default [
// route('auth/*', 'routes/auth.$.tsx'),
// ...
// ];
52 changes: 0 additions & 52 deletions src/server/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,55 +352,3 @@ export function stripIdTokenClaims(user: Auth0User): Auth0User {
) as Auth0User;
}

// ─── auth0Routes ──────────────────────────────────────────────────────────────

/**
* Returns a set of route objects for React Router that wire up the Auth0
* endpoints automatically. Spread these into your routes config so you don't
* need to create individual route files for each auth endpoint.
*
* Routes registered:
* GET /auth/login → handleLogin
* GET /auth/callback → handleCallback
* POST /auth/logout → handleLogout
* POST /auth/backchannel-logout → handleBackchannelLogout
*
* @example
* // app/routes.ts
* import { auth0Routes } from '@auth0/auth0-react-router/server';
* export default [...auth0Routes(auth0), ...appRoutes];
*/
export function auth0Routes(auth0: Auth0Server): Array<{
path: string;
loader?: (args: { request: Request }) => Promise<Response>;
action?: (args: { request: Request }) => Promise<Response>;
}> {
return [
{
path: '/auth/login',
loader: ({ request }) => handleLogin(auth0, request)
},
{
path: '/auth/callback',
loader: ({ request }) => handleCallback(auth0, request)
},
{
path: '/auth/logout',
loader: async () =>
new Response('Method Not Allowed', {
status: 405,
headers: { Allow: 'POST' }
}),
action: ({ request }) => handleLogout(auth0, request)
},
{
path: '/auth/backchannel-logout',
loader: async () =>
new Response('Method Not Allowed', {
status: 405,
headers: { Allow: 'POST' }
}),
action: ({ request }) => handleBackchannelLogout(auth0, request)
}
];
}
1 change: 0 additions & 1 deletion src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export {
handleLogout,
handleBackchannelLogout,
handleAuth,
auth0Routes,
stripIdTokenClaims
} from './handlers.js';
export type {
Expand Down
1 change: 0 additions & 1 deletion tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ export default defineConfig({
entry: {
index: 'src/client/index.ts',
'server/index': 'src/server/index.ts',
'routes/index': 'src/routes/index.ts',
'errors/index': 'src/errors/index.ts',
'types/index': 'src/types/index.ts',
'testing/index': 'src/testing/index.ts'
Expand Down