Skip to main content

Performing MFA

In the previous topic we closed on a block which looked like:

    // Verify a username/password factor.
    adaptive.evaluatePassword(context, req.session.transactionId, identitySourceId, username, password)
        .then((result) => {
            if (result.status == "deny") {
                res.status(403).send({ error: "Denied" });
                return
            }
            if (result.status == "allow") {
                res.redirect("/");
                return
            }

However, what this block does not cover, is that after a first factor authentication is performed, there is the potential for an MFA challenge to be returned. An MFA challenge can be detected by checking for a status which has the value "requires". A requires status indicates that some further authentication must be performed by the end user. There is potential for a requires status to be returned whenever /token is invoked, whether it be for establishing a grant, or refreshing a token.

Note: You can control when MFA is required by adding or adjusting the rules using the Policy Editor. For more information on policy see Native App Access policy.

In this example we handle a requires response, process the returned enrollments, and initiate email OTP if it is available:

if (result.status == "requires") {

    // result.enrolledFactors will contain all of our available enrollments, so break that down into type
    var availableFactors = result.enrolledFactors.map(f => f.type);

    // Save some session state
    req.session.transactionId = result.transactionId;

    // In this instance we're only supporting email OTP
    var emailIdx = availableFactors.indexOf("emailotp");
    if ( emailIdx != -1) {
        req.session.currentFactor = "emailotp";
        // Initiate the email OTP
        adaptive.generateEmailOTP(context, result.transactionId, result.enrolledFactors[emailIdx].id).then(() => {
            res.redirect("/otp");
        }).catch((error) => {
            res.status(500).send({ error: "Could not generate:" + error.message });
        });
    } else {
        res.status(500).send({ error: "No factors allowed are supported. " + JSON.stringify(result) });
    }
}

The following template can be used as a simple OTP template:

<html><head>
</head><body>
  <h1>Submit OTP</h1>
    <p>If the received code's format is 1234-567890 only enter the group of digits after the '-', i.e. 567890</p>
  <form name="otpform" action="/otp" method="POST"><label for="otp">OTP:</label><br>
    <input type="text" id="otp" name="otp"><br><input type="submit" value="Submit">
  </form>
</body></html>

Save it as otp.html and add it to the node application with:

app.get('/otp', (_, res) => {
    res.sendFile(__dirname + '/otp.html');
});

After initiating this email OTP, the user will need to perform the required steps, and then present the OTP back:

app.post("/otp", (req, res) => {

    const otp = req.body.otp;

    // Saved when we called assessPolicy
    var transactionId = req.session.transactionId;  

    var context = {
        sessionId : req.session.sessionId, // The session ID saved during evaluate
        userAgent : req.headers['user-agent'], // The user-agent collected from headers
        ipAddress : req.ip // The IP address of the connection.
    };

    // Verify the email OTP:
    adaptive.evaluateEmailOTP(context, transactionId, otp)
      .then((result) => {
        if (result.status == "deny") {
          res.status(403).send({ error: "Denied" });
          return
        }
        if (result.status == "allow") {
          req.session.token = result.token;
          res.redirect("/");
          return
        }
      }).catch((error) => {
        console.log(error);
        if (error.response.data.messageDescription) {
          res.status(403).send({ error: error.response.data.messageDescription });
        } else {
          res.status(403).send({error: error.message});
        }
      });
});

Once a full token is received by the end user, its up to the application developer to maintain session state for their application. Knowing that the user has been authenticated and their risk assessed.

When result.status is set to allow, we can trace the full result object and see we receive a complete bearer token:

{
  "status": "allow",
  "token": {
    "access_token": "gJduCYHPK0jG7RBiMSiQ3WtGQvxUzpp4GtqTZi8s",
    "scope": "openid",
    "grant_id": "24c6fa4e-dca5-4876-bd24-6b897920d74b",
    "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNlcnZlciJ9.eyJhY3IiOiAidXJuOmlibTpzZWN1cml0eTpwb2xpY3k6aWQ6MzIxNDc2IiwgInVzZXJUeXBlIjogInJlZ3VsYXIiLCAidW5pcXVlU2VjdXJpdHlOYW1lIjogIjYwNDAwMDRBR1IiLCAiZGlzcGxheU5hbWUiOiAidGVzdHVzZXIiLCAianRpIjogImhCcUJMQzFkRUxrbEYwbnJWcXF6a0Z4NFFwMnpVZSIsICJyZWFsbU5hbWUiOiAiY2xvdWRJZGVudGl0eVJlYWxtIiwgImF0X2hhc2giOiAiTWZ1SzYzVmJhVEpWeTBKUEtISjBrdyIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAidGVzdHVzZXJAYWNtZS5jb20iLCAiZXh0IjogeyJ0ZW5hbnRJZCI6ICJodHRwczovL3RlbmFudC5pY2UuaWJtY2xvdWQuY29tLyJ9LCAiaXNzIjogImh0dHBzOi8vdGVuYW50LmljZS5pYm1jbG91ZC5jb20vL29pZGMvZW5kcG9pbnQvZGVmYXVsdCIsICJhdWQiOiAiMWNmOGJiZmQtMDYzZC00ZmYyLTljN2UtN2ZlMjRhNmQ1OGEwIiwgInN1YiI6ICI2MDQwMDA0QUdSIiwgImlhdCI6IDE1OTkwOTQxNjcsICJleHAiOiAxNTk5MTAxMzY3fQ.HZ9LE3Hb3GkXD34ifok3XazHUzr1TU4c3X7T30t_JJ2tTTkINY6d3AIcN0ydhsrSosyW3L_rk3lKTJRAAF877o6E6vf83Jh146Ix3phTIcfxqWO9pf8IelKVrwKAyk71z_cTRBjIix5neh6Y0JziFEahBd12OUvVgVXyT9UJ6rlD6mo9Y4Pr_-Y-nBxvL1x8IgeJzHqlDdorXGPRTK8oCRGsqdbbHQhgOCop9TwswWC5pUQ8TOigTYjRK0bVdKm3ftJC-n4ghbeZI9VLT2cN8BLgzA6_JymIUF5hESaGI3zBqb2neAQD_c2iYApCuc6syDnC4rSdA7sQJFlXoXGP3w",
    "token_type": "Bearer",
    "expires_in": 7199
  }
}

If the application wishes to have a long lived session, such as supporting a 'remember me' function, or needs to use the access token to invoke APIs for a duration which may exceed the lifetime of the access token, a refresh token can be returned. See the next step for handling refresh token flows.


Next: Using Refresh Tokens

Previous: Using the Proxy SDK to Authenticate a User

See also: An end-to-end example application using the Browser and Proxy SDKs