Multifactor Authentication and Resource Owner Password

Heads up! As part of our efforts to improve security and standards-based interoperability, we have implemented several new features in our authentication flows and made changes to existing ones. For an overview of these changes, and details on how you adopt them, refer to Introducing OIDC Conformant Authentication.

Highly-trusted applications can use the Resource Owner Password Grant to access an API. The flow typically involves prompting the user for username and password as credentials to be submitted to Auth0. In some scenarios, however, stronger authentication may be required. This document outlines using multifactor authentication with the Resource Owner Password Grant.

Prerequisites

Before you continue, make sure that he following prerequisites apply:

  1. MFA is enabled at the Auth0 dashboard. Currently, the supported providers for this flow are Google Authenticator and Guardian. Duo Security is not supported.

  2. A Client is configured to execute the Resource Owner Password Grant (either password or password-realm grant types). For details on how to implement this, refer to Execute the Resource Owner Password Grant.

  3. End users are enrolled with MFA.

Initiate Multifactor Authentication

The flow starts by collecting end-user credentials and sending them to Auth0, as described in Resource Owner Password Grant. Both password and password-realm flows are available.

  1. The user enters their credentials into the Client application.

  2. The Client forwards the credentials to Auth0.

  3. Auth0 validates the credentials and executes any applicable rules.

  4. If any rule triggers MFA for the current user, an error code of mfa_required is returned. The error will additionally contain an mfa_token property.

HTTP/1.1 403 Forbidden
Content-Type: application/json
{
  "error": "mfa_required",
  "error_description": "Multifactor authentication required",
  "mfa_token": "eyJ0eXAiOiJKV1QiLCJhbGci....D3QCiQ"
}
  1. The Client will then make a request to the MFA challenge endpoint, specifying the challenge types it supports. Valid challenge types are: OTP, OOB and binding method prompt, OOB with no binding method. If you already know that otp is supported by the end-user and you don't want to request a different factor, you can skip this and the next steps an go directly to Challenge Type OTP below.

  2. Auth0 sends a response containing the challenge_type derived from the types supported by the Client and the specific user. Additionally, extra information, such as binding_method may be included to assist in resolving the challenge and displaying the correct UI to the user.

The supported challenge types are:

  • otp: A one-time password generated by an app setup with a seed, or by token generation hardware. This mechanism does not require an extra channel to prove possession; you can get it directly from the app / hardware device.

  • oob: The proof of possession is done 'out of band' via a side channel. There are several different channels, including push notification based authenticators and sms based authenticators. Depending on the channel and the authenticator chosen at enrollment, you may need to provide a binding_code used to bind the side channel and the channel used for authentication.

To execute MFA, follow the next steps according to the challenge type you will use:

  • OTP: for this challenge type your client application must prompt end-user for an otp code and continue the flow on the mfa-otp grant type.

  • OOB and binding method prompt: the challenge will be sent through a side channel (for example, SMS), and your client application will need to prompt the user for the binding_code (that was included as part of the challenge sent) and provide this code and the oob_code received as response for this request to prove possesion.

  • OOB with no binding method: in this case the proof of possession will be driven entirely in a side channel (for example, via a push notification based authenticator). The response will include an oob_code that the Client application will use to periodically check for the resolution of the transaction. Continue the flow on mfa-oob grant type.

Execute Multifactor

Challenge Type OTP

Resource Owner MFA OTP

For this type of challenge, the Client must get an otp code from a OTP Generator app such as Google Authenticator, Microsoft Authenticator, and so forth.

If it's already known that the user supports OTP, then steps 5 and 6 above are optional.

  1. The Client application prompts the end user to enter an otp code.

  2. The end user enters their otp into the Client application.

  3. The Client application forwards the otp code to Auth0 using grant_type=http://auth0.com/oauth/grant-type/mfa-otp and includes the mfa_token obtained in step 4 above.

  4. Auth0 validates the provided otp and returns the access_token and the refresh_token.

  5. The Client can use the access_token to call the API on behalf of the end user.

Challenge type OOB and binding method prompt

Resource Owner MFA OOB Prompt

This challenge type, together with prompt binding method, indicates that the challenge will be delivered to the user using a side channel (such as SMS) and that a binding_code is needed to bind the side channel to the one being authenticated. The binding code is sent as part of the challenge message and it is usually an otp-like code composed of 6 numeric digits.

  1. The Client application prompts the user for the binding_code and stores the oob_code from step 6 for future use.

  2. The end user receives the challenge on the side channel and enters the binding_code into the Client application.

  3. The Client application forwards the binding_code to Auth0 using grant_type=http://auth0.com/oauth/grant-type/mfa-oob and includes the mfa_token (from step 4) and oob_code (from step 6).

  4. Auth0 validates the binding_code and oob_code and returns the access_token and the refresh_token.

  5. The Client can use the access_token to call the API on behalf of the end user.

Challenge type OOB with no binding method

Resource Owner MFA OOB

In this scenario, the challenge will be sent using a side channel, however, there is no need for a binding_code. Currently, the only mechanism supported for this scenario is Push Notification with the Guardian Provider.

  1. The Client application asks the user to accept the delivered challenge and keeps the oob_code from step 6 for future use.

  2. The Client application polls Auth0 using grant_type=http://auth0.com/oauth/grant-type/mfa-oob and includes the mfa_token (from step 4) and oob_code (from step 6).

  3. Auth0 validates the provided oob_code, the mfa_token and returns:

    • pending_authentication error: if the challenge has not been accepted nor rejected.
    • slow_down error: if the polling is too frequent.
    • an access_token and a refresh_token: if the challenge has been accepted; polling should be stopped at this point.
    • invalid_grant error: if the challenge has been rejected; polling should be stopped at this point.
  4. The Client can use the access_token to call the API on behalf of the end user.

Using recovery codes

This flow is currently only available for the Guardian Provider.

Resource Owner MFA Recovery

Some providers support using a recovery code to login in case the enrolled device is not available, or if lack of connectivity prevents receiving an otp code or push notification.

Using a recovery code is similar to using an otp code to login. The main difference is that a new recovery code will be generated, and that the application must display this new recovery code to the user for secure storage.

Steps 1-4 are the same as above.

  1. End user chooses to use the recovery code.

  2. The Client prompts the end user to enter recovery code.

  3. The end user enters their recovery code into the Client application.

  4. The Client application forwards the recovery code to Auth0 using grant_type=http://auth0.com/oauth/grant-type/mfa-otp and includes the mfa_token from step 4.

  5. Auth0 validates the recovery code and returns the access_token and the refresh_token.

  6. The Client can use the access_token to call the API on behalf of the end user.

Examples

Resource Owner Password Grant Request

var request = require("request");

var options = { method: 'POST',
  url: 'https://YOUR_AUTH0_DOMAIN/oauth/token',
  headers: { 'content-type': 'application/json' },
  body:
   { grant_type: 'password',
     username: 'USERNAME',
     password: 'PASSWORD',
     audience: 'API_IDENTIFIER',
     scope: 'SCOPE',
     client_id: 'YOUR_CLIENT_ID',
     client_secret: 'YOUR_CLIENT_SECRET' },
  json: true };

request(options, function (error, response, body) {
  if (error) throw new Error(error);

  if (body.error === 'mfa_required') {
    // Show mfa flow and give user option to go to the recovery flow (if supported)

    if (/* MFA Recovery is requested*/) {
      const recovery_code = // Prompt for recovery code
      mfaRecovery(body.mfa_token, recovery_code) // See MFA Recovery grant
    } else {
      mfaChallenge(body.mfa_token)
    }
  }
});

Challenge Request

function mfaChallenge(mfa_token) {
  var options = { method: 'POST',
    url: 'https://YOUR_AUTH0_DOMAIN/mfa/challenge',
    headers: { 'content-type': 'application/json' },
    body:
    { mfa_token: mfa_token,
      challenge_type: 'oob otp', // Supported challenge types, space separated
      client_id: 'YOUR_CLIENT_ID',
      client_secret: 'YOUR_CLIENT_SECRET' },
    json: true };

  request(options, function (error, response, body) {
    if (error) throw new Error(error);

    if (body.challenge_type === 'otp') {
      const otp = // Prompt for otp code (see MFA OTP grant request)
      mfaOTP(mfa_token, otp)
    } else if (body.challenge_type === 'oob') {
      if (body.binding_method === 'prompt') {
        const binding_code = // Prompt for binding code (see MFA OOB with binding code grant request)
        mfaOOB(mfa_token, body.oob_code, binding_code)
      } else if (!body.binding_method) {
        // Ask the user to accept the challenge and start polling (see MFA OOB without binding code grant request)
        mfaOOB(mfa_token, body.oob_code)
      } else {
        console.error('Unsupported binding_method');
      }
    } else {
      console.error('Something went wrong');
    }
  });
}

MFA OTP Grant Request

function mfaOTP(mfa_token, otp) {
  var options = { method: 'POST',
    url: 'https://YOUR_AUTH0_DOMAIN/oauth/token',
    headers: { 'content-type': 'application/json' },
    body:
    { mfa_token: mfa_token,
      otp: otp,
      grant_type: 'http://auth0.com/oauth/grant-type/mfa-otp',
      client_id: 'YOUR_CLIENT_ID',
      client_secret: 'YOUR_CLIENT_SECRET' },
    json: true };

  request(options, function (error, response, body) {
    if (error) throw new Error(error);

    if (response.statusCode === 200) {
      // The tokens returned depend on the scopes requested on the password grant request
      console.log(body.access_token, body.id_token, body.refresh_token);
    } else if (body.error === 'invalid_grant') {
      // Invalid otp code
      console.error('Invalid otp');
    } else {
      console.error('Something went wrong');
    }
  });
}

MFA OOB Grant Request

function mfaOOB(mfa_token, oob_code, /* optional */ binding_code) {
  makeOOBGrantRequest(mfa_token, oob_code, binding_code, function(error, result) {
    if (error) { throw error; }

    if (result.state === 'authorization_pending') {
      // Poll every 10 seconds
      setTimeout(() => makeOOBGrantRequest(mfa_token, oob_code, binding_code), 10000);
    } else if (result.state === 'authorized')  {
      console.log(result.body.access_token, result.body.id_token, result.body.refresh_token);
    } else {
      console.error('You are not authorized')
    }
  });
}

function makeOOBGrantRequest(mfa_token, oob_code, /* optional  */ binding_code, cb) {
  var options = { method: 'POST',
    url: 'https://YOUR_AUTH0_DOMAIN/oauth/token',
    headers: { 'content-type': 'application/json' },
    body:
    { mfa_token: mfa_token,
      oob_code: oob_code,
      binding_code: binding_code, // Only when binding_method = prompt
      grant_type: 'http://auth0.com/oauth/grant-type/mfa-oob',
      client_id: 'YOUR_CLIENT_ID',
      client_secret: 'YOUR_CLIENT_SECRET' },
    json: true };

  request(options, function (error, response, body) {
    if (error) { return cb(error); }

    if (response.statusCode === 200) {
      // The tokens returned depend on the scopes requested on the password grant request
      cb(null, { state: 'authorized', body });
    } else if (body.error === 'invalid_grant') {
      // Invalid otp code
      cb(null, { state: 'not_authorized' });
    } else if (body.error === 'authorization_pending') {
      cb(null, { state: 'authorization_pending' });
    } else if (body.error === 'slow_down') {
      // You are polling too fast, slow down the polling rate,
      // You may want to check rate-limiting headers to manage your polling rate
      setTimeout(() => cb({ state: 'authorization_pending' }), 20000);
    } else {
      cb(new Error('Something went wrong'))
    }
  });
}

MFA Recovery Grant Request

function mfaRecovery(mfa_token, recovery_code) {
  var options = { method: 'POST',
    url: 'https://YOUR_AUTH0_DOMAIN/oauth/token',
    headers: { 'content-type': 'application/json' },
    body:
    { mfa_token: mfa_token,
      recovery_code: recovery_code,
      otp: otp,
      grant_type: 'http://auth0.com/oauth/grant-type/mfa-recovery-code',
      client_id: 'YOUR_CLIENT_ID',
      client_secret: 'YOUR_CLIENT_SECRET' },
    json: true };

  request(options, function (error, response, body) {
    if (error) throw new Error(error);

    if (response.statusCode === 200) {
      console.log('Please store this new recovery code safely -- the previous code will no longer work.', body.recovery_code)

      // The tokens returned depend on the scopes requested on the password grant request
      console.log(body.access_token, body.id_token, body.refresh_token);
    } else if (body.error === 'invalid_grant') {
      // Invalid otp code
      console.error('Invalid recovery_code');
    } else {
      console.error('Something went wrong');
    }
  });
}