Repository: auth0/nextjs-auth0
Pull request #24 (2019-10-14 14:47:08)

Support sending custom params
### Description

This change allows you to send custom parameters to Auth0 as part of the login transaction:

```js
import auth0 from '../../utils/auth0';

export default async function login(req, res) {
  try {
    await auth0.handleLogin(req, res, {
      authParams: {
        login_hint: 'foo@acme.com',
        ui_locales: 'nl',
        scope: 'some other scope',
        foo: 'bar'
      }
    });
  } catch(error) {
    console.error(error)
    res.status(error.status || 400).end(error.message)
  }
}
```


Patch:
diff --git a/README.md b/README.md
index 9f443c5a..54863837 100644
--- a/README.md
+++ b/README.md
@@ -107,6 +107,32 @@ export default async function callback(req, res) {
 }
 ```
 
+You can optionally send extra parameters to Auth0 to influence the transaction, for example:
+
+- Showing the login page
+- Filling in the user's email address
+- Exposing information to the custom login page (eg: to show the signup tab)
+
+```js
+import auth0 from '../../utils/auth0';
+
+export default async function login(req, res) {
+  try {
+    await auth0.handleLogin(req, res, {
+      authParams: {
+        login_hint: 'foo@acme.com',
+        ui_locales: 'nl',
+        scope: 'some other scope',
+        foo: 'bar'
+      }
+    });
+  } catch(error) {
+    console.error(error)
+    res.status(error.status || 400).end(error.message)
+  }
+}
+```
+
 ### Logout
 
 For signing the user out we'll also need a logout link:
diff --git a/src/handlers/callback-options.ts b/src/handlers/callback-options.ts
deleted file mode 100644
index 93193e0f..00000000
--- a/src/handlers/callback-options.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export type CallbackOptions = {
-  redirectTo?: string;
-};
diff --git a/src/handlers/callback.ts b/src/handlers/callback.ts
index bc692bcb..80a46582 100644
--- a/src/handlers/callback.ts
+++ b/src/handlers/callback.ts
@@ -5,7 +5,10 @@ import { ISession } from '../session/session';
 import { parseCookies } from '../utils/cookies';
 import { ISessionStore } from '../session/store';
 import { IOidcClientFactory } from '../utils/oidc-client';
-import { CallbackOptions } from './callback-options';
+
+export type CallbackOptions = {
+  redirectTo?: string;
+};
 
 export default function callbackHandler(
   settings: IAuth0Settings,
diff --git a/src/handlers/login.ts b/src/handlers/login.ts
index a0df19bb..cff10cce 100644
--- a/src/handlers/login.ts
+++ b/src/handlers/login.ts
@@ -22,8 +22,24 @@ function telemetry(): string {
     .replace(/=+$/, '');
 }
 
+export interface AuthorizationParameters {
+  acr_values?: string;
+  audience?: string;
+  display?: string;
+  login_hint?: string;
+  max_age?: string;
+  prompt?: string;
+  scope?: string;
+  ui_locales?: string;
+  [key: string]: unknown;
+}
+
+export interface LoginOptions {
+  authParams?: AuthorizationParameters;
+}
+
 export default function loginHandler(settings: IAuth0Settings, clientProvider: IOidcClientFactory) {
-  return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
+  return async (req: IncomingMessage, res: ServerResponse, options: LoginOptions = {}): Promise<void> => {
     if (!res) {
       throw new Error('Response is not available');
     }
@@ -33,11 +49,15 @@ export default function loginHandler(settings: IAuth0Settings, clientProvider: I
 
     // Create the authorization url.
     const client = await clientProvider();
+    const authParams = (options && options.authParams) || {};
     const authorizationUrl = client.authorizationUrl({
       redirect_uri: settings.redirectUri,
       scope: settings.scope,
       response_type: 'code',
-      audience: settings.audience
+      audience: settings.audience,
+      state,
+      auth0Client: telemetry(),
+      ...authParams
     });
 
     // Set the necessary cookies
@@ -51,7 +71,7 @@ export default function loginHandler(settings: IAuth0Settings, clientProvider: I
 
     // Redirect to the authorize endpoint.
     res.writeHead(302, {
-      Location: `${authorizationUrl}&state=${state}&auth0Client=${telemetry()}`
+      Location: authorizationUrl
     });
     res.end();
   };


Test patch:
diff --git a/tests/handlers/login.test.ts b/tests/handlers/login.test.ts
index 265dad2e..7819e625 100644
--- a/tests/handlers/login.test.ts
+++ b/tests/handlers/login.test.ts
@@ -2,25 +2,29 @@ import request from 'request';
 import { parse } from 'cookie';
 import { promisify } from 'util';
 
-import login from '../../src/handlers/login';
-import getClient from '../../src/utils/oidc-client';
-
 import HttpServer from '../helpers/server';
 import { discovery } from '../helpers/oidc-nocks';
+import getClient from '../../src/utils/oidc-client';
 import { withoutApi, withApi } from '../helpers/default-settings';
+import login, { LoginOptions } from '../../src/handlers/login';
+
 
 const [getAsync] = [request.get].map(promisify);
 
 describe('login handler', () => {
   let httpServer: HttpServer;
+  let loginHandler: any;
+  let loginOptions: LoginOptions | null;
 
-  beforeAll((done) => {
+  beforeEach((done) => {
     discovery(withoutApi);
-    httpServer = new HttpServer(login(withoutApi, getClient(withoutApi)));
+    loginOptions = null;
+    loginHandler = login(withoutApi, getClient(withoutApi));
+    httpServer = new HttpServer((req, res) => loginHandler(req, res, loginOptions));
     httpServer.start(done);
   });
 
-  afterAll((done) => {
+  afterEach((done) => {
     httpServer.stop(done);
   });
 
@@ -49,18 +53,52 @@ describe('login handler', () => {
         + `&response_type=code&redirect_uri=${encodeURIComponent(withoutApi.redirectUri)}`
         + `&state=${state['a0:state']}`);
   });
+
+  test('should contain the telemetry querystring', async () => {
+    const { statusCode, headers } = await getAsync({
+      url: httpServer.getUrl(),
+      followRedirect: false
+    });
+    expect(statusCode).toBe(302);
+    expect(headers.location)
+      .toContain('&auth0Client=');
+  });
+
+  test('should allow sending custom parameters to the authorization server', async () => {
+    loginOptions = {
+      authParams: {
+        max_age: '123',
+        login_hint: 'foo@acme.com',
+        ui_locales: 'nl',
+        scope: 'some other scope',
+        foo: 'bar'
+      }
+    };
+    const { statusCode, headers } = await getAsync({
+      url: httpServer.getUrl(),
+      followRedirect: false
+    });
+
+    expect(statusCode).toBe(302);
+    expect(headers.location)
+      .toContain(`https://${withoutApi.domain}/authorize?`
+        + `client_id=${withoutApi.clientId}&scope=${encodeURIComponent(withoutApi.scope)}`
+        + `&response_type=code&redirect_uri=${encodeURIComponent(withoutApi.redirectUri)}`);
+    expect(headers.location)
+      .toContain('&max_age=123&foo=bar');
+  });
 });
 
 describe('withApi login handler', () => {
   let httpServer: HttpServer;
 
-  beforeAll(done => {
+  beforeAll((done) => {
     discovery(withApi);
     httpServer = new HttpServer(login(withApi, getClient(withApi)));
     httpServer.start(done);
   });
 
-  afterAll(done => {
+  afterAll((done) => {
     httpServer.stop(done);
   });
 
@@ -84,11 +122,11 @@ describe('withApi login handler', () => {
 
     const state = parse(headers['set-cookie'][0]);
     expect(headers.location).toContain(
-      `https://${withApi.domain}/authorize?` +
-        `client_id=${withApi.clientId}&scope=${encodeURIComponent(withApi.scope)}` +
-        `&response_type=code&redirect_uri=${encodeURIComponent(withApi.redirectUri)}` +
-        `&audience=${encodeURIComponent(withApi.audience)}` +
-        `&state=${state['a0:state']}`
+      `https://${withApi.domain}/authorize?`
+      + `client_id=${withApi.clientId}&scope=${encodeURIComponent(withApi.scope)}`
+      + `&response_type=code&redirect_uri=${encodeURIComponent(withApi.redirectUri)}`
+      + `&audience=${encodeURIComponent(withApi.audience)}`
+      + `&state=${state['a0:state']}`
     );
   });
 });