Skip to content

Commit

Permalink
Merge pull request #840 from auth0/feature/custom-handler-config
Browse files Browse the repository at this point in the history
Add support for configuring the default handlers [SDK-3566]
  • Loading branch information
adamjmcgrath committed Oct 6, 2022
2 parents eeef5d0 + 13a3a0c commit f594c3a
Show file tree
Hide file tree
Showing 15 changed files with 735 additions and 146 deletions.
87 changes: 31 additions & 56 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,21 +91,27 @@ import { myCustomLogger, myCustomErrorReporter } from '../utils';

export default handleAuth({
async login(req, res) {
try {
// Add your own custom logger
myCustomLogger('Logging in');
// Pass custom parameters to login
await handleLogin(req, res, {
authorizationParams: {
custom_param: 'custom'
},
returnTo: '/custom-page'
});
} catch (error) {
// Add your own custom error handling
myCustomErrorReporter(error);
res.status(error.status || 400).end(error.message);
// Add your own custom logger
myCustomLogger('Logging in');
// Pass custom parameters to login
await handleLogin(req, res, {
authorizationParams: {
custom_param: 'custom'
},
returnTo: '/custom-page'
});
},
invite: loginHandler({
authorizationParams: (req) => {
invitation: req.query.invitation;
}
}),
'login-with-google': loginHandler({ authorizationParams: { connection: 'google' } }),
'refresh-profile': profileHandler({ refetch: true }),
onError(req, res, error) {
// Add your own custom error handling
myCustomErrorReporter(error);
res.status(error.status || 400).end();
}
});
```
Expand Down Expand Up @@ -258,19 +264,13 @@ Get an Access Token by providing your API's audience and scopes. You can pass th
import { handleAuth, handleLogin } from '@auth0/nextjs-auth0';

export default handleAuth({
async login(req, res) {
try {
await handleLogin(req, res, {
authorizationParams: {
audience: 'https://api.example.com/products', // or AUTH0_AUDIENCE
// Add the `offline_access` scope to also get a Refresh Token
scope: 'openid profile email read:products' // or AUTH0_SCOPE
}
});
} catch (error) {
res.status(error.status || 400).end(error.message);
login: handleLogin({
authorizationParams: {
audience: 'https://api.example.com/products', // or AUTH0_AUDIENCE
// Add the `offline_access` scope to also get a Refresh Token
scope: 'openid profile email read:products' // or AUTH0_SCOPE
}
}
})
});
```

Expand Down Expand Up @@ -378,41 +378,16 @@ Pass a custom authorize parameter to the login handler in a custom route.
If you are using the [New Universal Login Experience](https://auth0.com/docs/universal-login/new-experience) you can pass the `screen_hint` parameter.

```js
// api/signup.js
import { handleLogin } from '@auth0/nextjs-auth0';

export default async function signup(req, res) {
try {
await handleLogin(req, res, {
authorizationParams: {
// Note that this can be combined with prompt=login , which indicates if
// you want to always show the authentication page or you want to skip
// if there’s an existing session.
screen_hint: 'signup'
}
});
} catch (error) {
res.status(error.status || 400).end(error.message);
}
}
```

If you are using the [Classic Universal Login Experience](https://auth0.com/docs/universal-login/classic-experience) you can use any custom authorization
parameter, eg `{ authorizationParams: { action: 'signup' } }` then customize the
[login template](https://manage.auth0.com/#/login_page) to look for this parameter
and set the `initialScreen` option of the `Auth0Lock` constructor.
// pages/api/auth/[...auth0].js
import { handleAuth, handleLogin } from '@auth0/nextjs-auth0';

```js
var isSignup = config.extraParams && config.extraParams.action === 'signup';
var lock = new Auth0Lock(config.clientID, config.auth0Domain, {
// [...] all other Lock options
// use the value obtained to decide the first screen
initialScreen: isSignup ? 'signUp' : 'login'
export default handleAuth({
signup: handleLogin({ authorizationParams: { screen_hint: 'signup' } })
});
```

Users can then sign up using the signup handler.

```html
<a href="/api/signup">Sign up</a>
<a href="/api/auth/signup">Sign up</a>
```
68 changes: 64 additions & 4 deletions V2_MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ Guide to migrating from `1.x` to `2.x`
- [Profile API route no longer returns a 401](#profile-api-route-no-longer-returns-a-401)
- [The ID token is no longer stored by default](#the-id-token-is-no-longer-stored-by-default)
- [Override default error handler](#override-default-error-handler)
- [afterCallback can write to the response](#afterCallback-can-write-to-the-response)
- [afterCallback can write to the response](#aftercallback-can-write-to-the-response)
- [Configure default handlers](#configure-default-handlers)

## `getSession` now returns a `Promise`

Expand Down Expand Up @@ -120,6 +121,8 @@ You can choose to store it by setting either the `session.storeIDToken` config p

You can now set the default error handler for the auth routes in a single place.

### Before

```js
export default handleAuth({
async login(req, res) {
Expand Down Expand Up @@ -160,7 +163,7 @@ export default handleAuth({

## `afterCallback` can write to the response

You can now write your own redirect header or terminate the request in `afterCallback`
You can now write your own redirect header or terminate the request in `afterCallback`.

### Before

Expand All @@ -171,14 +174,14 @@ const afterCallback = (req, res, session, state) => {
} else {
res.status(401).end('User is not admin');
}
}; // 💥Fail with ERR_HTTP_HEADERS_SENT
}; // 💥 Fails with ERR_HTTP_HEADERS_SENT

const afterCallback = (req, res, session, state) => {
if (!session.user.isAdmin) {
res.setHeader('Location', '/admin');
}
return session;
}; // 💥Fail with ERR_HTTP_HEADERS_SENT
}; // 💥 Fails with ERR_HTTP_HEADERS_SENT
```

### After
Expand All @@ -199,3 +202,60 @@ const afterCallback = (req, res, session, state) => {
return session;
}; // Redirects to `/admin` if user is admin
```

## Configure default handlers

Previously it was not possible to configure the default handlers. For example, to pass a `connection` parameter to the login handler, you had to override it.

### Before

```js
export default handleAuth({
login: async (req, res) => {
try {
await handleLogin(req, res, {
authorizationParams: { connection: 'github' },
});
} catch (error) {
// ...
}
}
});
```

### After

Now you can configure a default handler by passing an options object to it.

```js
export default handleAuth({
login: handleLogin({
authorizationParams: { connection: 'github' }
})
});
```

You can also pass a function that receives the request and returns an options object.

```js
export default handleAuth({
login: handleLogin((req) => {
return {
authorizationParams: { connection: 'github' }
};
})
});
```

You can even create new handlers by configuring the default ones.

```js
export default handleAuth({
// Creates /api/auth/signup
signup: handleLogin({
authorizationParams: { screen_hint: 'signup' }
})
});
```

It is still possible to override the default handlers if needed.
75 changes: 48 additions & 27 deletions src/handlers/auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next';
import { HandleLogin } from './login';
import { HandleLogout } from './logout';
import { HandleCallback } from './callback';
import { HandleProfile } from './profile';
import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next';
import { HandlerError } from '../utils/errors';

/**
Expand All @@ -24,28 +24,55 @@ import { HandlerError } from '../utils/errors';
* } catch (error) {
* // Add you own custom error logging.
* errorReporter(error);
* res.status(error.status || 500).end(error.message);
* res.status(error.status || 500).end();
* }
* }
* });
* ```
*
* Alternatively, you can customize the default handlers without overriding them. For example:
*
* ```js
* // pages/api/auth/[...auth0].js
* import { handleAuth, handleLogin } from '@auth0/nextjs-auth0';
*
* export default handleAuth({
* login: handleLogin({
* authorizationParams: { customParam: 'foo' } // Pass in custom params
* })
* });
* ```
*
* You can also create new handlers by customizing the default ones. For example:
*
* ```js
* // pages/api/auth/[...auth0].js
* import { handleAuth, handleLogin } from '@auth0/nextjs-auth0';
*
* export default handleAuth({
* signup: handleLogin({
* authorizationParams: { screen_hint: 'signup' }
* })
* });
* ```
*
* @category Server
*/
export interface Handlers {
login: HandleLogin;
logout: HandleLogout;
callback: HandleCallback;
profile: HandleProfile;
onError: OnError;
}
export type Handlers = ApiHandlers | ErrorHandlers;

type ApiHandlers = {
[key: string]: NextApiHandler;
};

type ErrorHandlers = {
onError?: OnError;
};

/**
* The main way to use the server SDK.
*
* Simply set the environment variables per {@link ConfigParameters} then create the file
* `pages/api/auth/[...auth0].js`.
* For example:
* `pages/api/auth/[...auth0].js`. For example:
*
* ```js
* // pages/api/auth/[...auth0].js
Expand All @@ -63,7 +90,7 @@ export interface Handlers {
*
* @category Server
*/
export type HandleAuth = (userHandlers?: Partial<Handlers>) => NextApiHandler;
export type HandleAuth = (userHandlers?: Handlers) => NextApiHandler;

export type OnError = (req: NextApiRequest, res: NextApiResponse, error: HandlerError) => Promise<void> | void;

Expand All @@ -89,12 +116,12 @@ export default function handlerFactory({
handleCallback: HandleCallback;
handleProfile: HandleProfile;
}): HandleAuth {
return ({ onError, ...handlers }: Partial<Handlers> = {}): NextApiHandler<void> => {
const { login, logout, callback, profile } = {
return ({ onError, ...handlers }: Handlers = {}): NextApiHandler<void> => {
const customHandlers: ApiHandlers = {
login: handleLogin,
logout: handleLogout,
callback: handleCallback,
profile: handleProfile,
me: (handlers as ApiHandlers).profile || handleProfile,
...handlers
};
return async (req, res): Promise<void> => {
Expand All @@ -105,20 +132,14 @@ export default function handlerFactory({
route = Array.isArray(route) ? route[0] : /* c8 ignore next */ route;

try {
switch (route) {
case 'login':
return await login(req, res);
case 'logout':
return await logout(req, res);
case 'callback':
return await callback(req, res);
case 'me':
return await profile(req, res);
default:
res.status(404).end();
const handler = route && customHandlers[route];
if (handler) {
await handler(req, res);
} else {
res.status(404).end();
}
} catch (error) {
await (onError || defaultOnError)(req, res, error);
await (onError || defaultOnError)(req, res, error as HandlerError);
if (!res.writableEnded) {
// 200 is the default, so we assume it has not been set in the custom error handler if it equals 200
res.status(res.statusCode === 200 ? 500 : res.statusCode).end();
Expand Down
Loading

0 comments on commit f594c3a

Please sign in to comment.