Skip to main content

NEVIS Tutorial How to Account Recovery

Overview

The following tutorial describes how to configure an account recovery flow in your NEVIS infrastructure. You need such a flow when users cannot log in with their second factor credentials, for example because they bought a new phone. This flow requires the users to enter their primary credentials first (in this example, this is the password). They can then recover access to their account by using mTAN or recovery code credentials.

If users loose their password credentials, they have to initiate a password reset flow that is not part of this tutorial. For more information about how to access these credentials via the REST API of nevisIDM, see the separate documentation.

This guide lists all the necessary NEVIS configurations and code snippets that you need to be able to construct an account recovery flow. The description starts from the point where the user initiated an account recovery, for example by pressing a button on a login page.

The following screenshot demonstrates what you can build after reading this guide:

GUIs you can build after reading this guide

This tutorial focuses on recovering access to accounts using mTAN and recovery code credentials. But after reading this document, you will also be able to add other credential types to the flow, for example OTP.

Prerequisites

Software versions

Make sure you are running at least the following versions:

  • nevisIDM: 2.78.0+
  • nevisAuth: 4.23.0+

For more details about compatible versions and other requirements, have a look at our lifetime page in the guide "NEVIS Product Lifetime and Platform Support Matrix" .

nevisIDM configuration

Enable the required credential types (password, mTAN, recovery code) in your nevisIDM configuration for the tutorial example to work:

The client policy parameter availableCredentialTypes specifies the available credentials in your clients. See chapter "Client policy" for more details. To enable the password, mTAN and recovery code credentials in your clients, use the following client policy configuration: availableCredentialTypes=[1,11,22] The unit policy parameter credentialTypes specifies the available credentials in your units. See chapter "Unit policy" for more details. To enable the password, mTAN and recovery code credentials in your units, use the following unit policy configuration: credentialTypes=[1,11,22]

Flow Configuration

The scripts and configurations described in the following sections work if a user has one of the mTAN and recovery code credentials, or both. If a user has none, the flow restarts and an error is displayed to the user.

To understand this section, be familiar with the basic configuration building blocks and mechanisms of NEVIS authentication. For example, know what an AuthState is, or how the authentication engine works. To read more about these topics, check the chapter "Authentication Plug-Ins (AuthStates)" in the nevisIDM reference guide, and the chapter "Authentication Plug-Ins and AuthStates" in the nevisAuth reference guide.

The following flow is just an example of how you can create an account recovery flow. You can modify the flow based on your needs. For example, you can extend it with additional credential types.

Entry point

To be able to trigger the account recovery flow, you need to create an entry point (a specific URL) in your authentication engine's configuration. This configuration is attached to your nevisAuth instance's configuration, which contains an AuthEngine tag where you can specify a new Entry within your Domain, like <Entry method="authenticate" state="IdmUserIdPasswordLoginForAccountRecovery" selector="/nevis/login/recover-account" />. This entry point is where you have to navigate your users after they clicked the Recovery Account button in the login screen.

Description

AuthState flow

The following steps describe how users can gain access to their accounts via the recovery flow:

  • The user navigates to the entry point of the flow (nevis/login/recover-account).
<Entry method="authenticate" state="IdmUserIdPasswordLoginForAccountRecovery" selector="/nevis/login/recover-account" />
  • The AuthState IdmUserIdPasswordLoginForAccountRecovery is triggered first. Here, the user has to enter their username and password credentials.
    • The AuthState can also display an error, if the user has neither mTAN nor recovery code credentials (for details, see the following IdmRestGetUsermTanAndRecoveryCodeCredentials AuthState).
<AuthState name="IdmUserIdPasswordLoginForAccountRecovery" class="ch.nevis.idm.authstate.IdmPasswordVerifyState" authLevel="auth.weak">
<ResultCond name="ok" next="IdmGetUserInformationForAccountRecovery"/>
<ResultCond name="default" next="IdmUserIdPasswordLoginForAccountRecovery"/>
<ResultCond name="failed" next="IdmUserIdPasswordLoginForAccountRecovery"/>
<Response value="AUTH_CONTINUE">
<Gui name="AuthUidPwDialog" label="login.uidpw.label">
<GuiElem name="lasterror" type="error" label="${notes.lasterrorinfo}" value="${notes.lasterror}"/>
<GuiElem name="restError" type="error" label="${notes.noCredential}" value="${notes.noCredential}"/>
<GuiElem name="client" type="text" label="client.label" value="${notes.client}"/>
<GuiElem name="isiwebuserid" type="text" label="userid.label" value="${notes.loginid}"/>
<GuiElem name="isiwebpasswd" type="pw-text" label="password.label"/>
<GuiElem name="submit" type="button" label="submit.button.label" value="Login"/>
</Gui>
</Response>
<property name="user.loginType" value="AUTO"/>
<property name="detaillevel.user" value="MEDIUM" />
</AuthState>
  • After a successful password verification, the AuthState IdmGetUserInformationForAccountRecovery is triggered, to fetch the user's information.
<AuthState name="IdmGetUserInformationForAccountRecovery" class="ch.nevis.idm.authstate.IdmGetPropertiesState" final="false">
<ResultCond name="ok" next="IdmRestGetUsermTanAndRecoveryCodeCredentials"/>
<ResultCond name="default" next="IdmGetUserInformationForAccountRecovery"/>
<Response value="AUTH_CONTINUE">
<Gui name="AuthError">
<GuiElem name="lasterror" type="error" label="${notes.lasterrorinfo}" value="${notes.lasterror}"/>
</Gui>
</Response>
</AuthState>
  • The next AuthState, the IdmRestGetUsermTanAndRecoveryCodeCredentials state, runs the get-mTan-and-recoveryCode-credentials.groovy script to fetch all the mTAN and recovery code credentials of the user, via the REST API of the nevisIDM instance. For more details about the nevisIDM REST API, see the separate documentation.
    • If the user has neither mTAN nor recovery code credentials, then the AuthState writes a corresponding message in the notes, returns the noSuitableCredentials transition and navigates back to the first AuthState (IdmUserIdPasswordLoginForAccountRecovery), which displays the error message from the notes to the user.
    • If the user has one or both of the required credentials, the AuthState returns the ok transition. The authentication flow continues.
    • The AuthState writes the user's credential information into the ch.session.nevis.mtan.credentials and/or the ch.session.nevis.recoveryCode.credential session properties, depending on which credential(s) the user has.
    • The AuthState requires the property parameter.baseUrl, which specifies the URL of the REST API of nevisIDM, to be able to send requests to the API.
<AuthState name="IdmRestGetUsermTanAndRecoveryCodeCredentials" class="ch.nevis.esauth.auth.states.scripting.ScriptState" final="false">
<ResultCond name="ok" next="ShowAccountRecoveryOptions"/>
<ResultCond name="noSuitableCredentials" next="IdmUserIdPasswordLoginForAccountRecovery"/>
<property name="script" value="file:///var/opt/nevisauth/default/conf/get-mTan-and-recoveryCode-credentials.groovy"/>
<property name="parameter.baseUrl" value="https://__idm-hostname__:__port__/nevisidm/api" />
</AuthState>
import ch.nevis.idm.client.IdmRestClient
import ch.nevis.idm.client.IdmRestClientFactory
import groovy.json.*

String userBaseUrl = parameters.get('baseUrl') + '/' + 'core/v1/' + session.get('ch.adnovum.nevisidm.clientId') + '/users/' + session.get('ch.adnovum.nevisidm.userExtId');
String mTanGetUrl = userBaseUrl + '/mtans';
String recoveryCodeGetUrl = userBaseUrl + '/recovery-codes';
IdmRestClient idmRestClient = new IdmRestClientFactory().getInstance();

boolean hasMtanCredentials;
boolean hasRecoveryCodeCredential;
try {
String mTanGetResult = idmRestClient.get(mTanGetUrl);
def userMTanCredentials = new JsonSlurper().parseText(mTanGetResult);
hasMtanCredentials = userMTanCredentials && userMTanCredentials.items;

if (hasMtanCredentials) {
LOG.debug('user has mTan credentials');
session.put('ch.session.nevis.mtan.credentials', getMobileNumbersAndExtIds(userMTanCredentials.items));
response.setResult('ok');
}
}
catch(Exception e) {
LOG.debug('user has no mTan credentials,' + e.getMessage());
}

try {
String recoveryCodeGetResult = idmRestClient.get(recoveryCodeGetUrl);
hasRecoveryCodeCredential = recoveryCodeGetResult;

if (hasRecoveryCodeCredential) {
LOG.debug('user has Recovery code credential');
session.put('ch.session.nevis.recoveryCode.credential', true);
response.setResult('ok');
}
}
catch(Exception e) {
LOG.debug('user has no Recovery code credential,' + e.getMessage());
}

if (!hasMtanCredentials && !hasRecoveryCodeCredential) {
LOG.debug('user has neither mTan nor Recovery code credentials');
notes.setProperty('noCredential', 'User has neither mTan nor Recovery code credential to recover the account.');
response.setResult('noSuitableCredentials');
}

def static getMobileNumbersAndExtIds(userMTans) {
def phoneNumbersMap = [:];
for (int i = 0; i < userMTans.size(); i++) {
def mTanCredential = userMTans[i];
if (mTanCredential.mobileNumber.e164) {
// leading 00 is needed for the SMS gateway
phoneNumbersMap.put(mTanCredential.mobileNumber.e164.replace('+', '00'), mTanCredential.extId);
}
else {
phoneNumbersMap.put(mTanCredential.mobileNumber.raw, mTanCredential.extId);
}
}

return phoneNumbersMap;
}
  • The next AuthState ShowAccountRecoveryOptions uses the show-account-recovery-options.groovy script to display a GUI with radio buttons for the phone numbers and the recovery code option.
    • Which radio buttons are displayed is based on the properties ch.session.nevis.mtan.credentials and ch.session.nevis.recoveryCode.credential in the session.
      • In case of mTAN credentials, the AuthState displays radio buttons with masked phone numbers of the mTAN credentials of the user. An example of how to mask the phone number is done in the formatNumber method of the show-account-recovery-options Groovy script.
      • If the user has a recovery code credential, the AuthState displays a radio button for recovery code login.
    • Also the text in the GUI depends on the credentials of the user, that is, on the properties ch.session.nevis.mtan.credentials and ch.session.nevis.recoveryCode.credential in the session.
    • If the user selects one of their phone numbers in case of an mTAN credential, the AuthState writes the values of the properties ch.nevis.idm.mtan.credential.selected.mobile and ch.nevis.idm.mtan.credential.selected.extId to the session. These values are needed to be able to uniquely identify the selected mTAN credential.
    • Then the AuthState returns either the loginWithmTan transition or the loginWithRecoveryCode transition, based on the user's choice.
<AuthState name="ShowAccountRecoveryOptions" class="ch.nevis.esauth.auth.states.scripting.ScriptState" final="false">
<ResultCond name="loginWithmTan" next="IdmMultipleMTanLoginRest"/>
<ResultCond name="loginWithRecoveryCode" next="LoginWithRecoveryCode"/>
<Response value="AUTH_CONTINUE">
<Gui name="chooseAccountRecoveryOption"/>
</Response>
<property name="script" value="file:///var/opt/nevisauth/default/conf/show-account-recovery-options.groovy"/>
</AuthState>
if (inargs.getProperty('submit') == 'continue' && inargs.getProperty('ch.nevis.idm.recoveryAccount.credential')) {

boolean recoveryCodeSelected;
def selectedCredential = inargs.getProperty('ch.nevis.idm.recoveryAccount.credential');
if (selectedCredential == 'Recovery code') {
recoveryCodeSelected = true;
}

if (recoveryCodeSelected) {
LOG.debug('user selected Recovery code credential to recover the account');
response.setResult('loginWithRecoveryCode');
}
else {
LOG.debug('user selected mTan credential to recover the account');
def selectedMobile = selectedCredential.split(';');
session.put('ch.nevis.idm.mtan.credential.selected.mobile', selectedMobile[0]);
session.put('ch.nevis.idm.mtan.credential.selected.extId', selectedMobile[1]);
response.setResult('loginWithmTan');
}

return;
}

createRadioButtons();

def static formatNumber(String phoneNumber) {
if (phoneNumber.length() < 4) {
return phoneNumber;
}
return phoneNumber[0..5] + phoneNumber[5..-3].replaceAll(/[^\s]/, "*") + phoneNumber[-2..-1];
}

def createRadioButtons() {
def phoneNumbers = session.get('ch.session.nevis.mtan.credentials');
def hasRecoveryCodeCredential = session.get('ch.session.nevis.recoveryCode.credential');

response.setGuiName('chooseAccountRecoveryOption');
if (phoneNumbers && hasRecoveryCodeCredential) {
response.setGuiLabel('Choose a phone number or the recovery code option to recover your account');
}
else if (phoneNumbers) {
response.setGuiLabel('Choose a phone number to recover your account');
}
else {
response.setGuiLabel('Choose the recovery code option to recover your account');
}

if (phoneNumbers) {
response.addInfoGuiField('mTanDivider', '<hr>', '');
for (phoneNumber in phoneNumbers) {
response.addRadioGuiField('ch.nevis.idm.recoveryAccount.credential', formatNumber(phoneNumber.key), phoneNumber.key + ';' + phoneNumber.value);
}
}
if (hasRecoveryCodeCredential) {
response.addInfoGuiField('recoveryCodeDivider', '<hr>', '');
response.addRadioGuiField('ch.nevis.idm.recoveryAccount.credential', 'Recovery code', 'Recovery code');
}
response.addButtonGuiField('submit', 'submit', 'continue');

}
  • In case of a loginWithmTan transition, that is, when the user selected one of their phone numbers, the IdmMultipleMTanLoginRest AuthState is called; an SMS token is sent to the selected phone number (session:ch.nevis.idm.mtan.credential.selected.mobile). The IdmMultipleMTanLoginRest AuthState is a TANState type of AuthState. This example TANState configuration uses the SwissPhone channel type. For a detailed description of the TANState, see the chapter "TAN authentication plug-ins" in the nevisAuth reference guide.
    • After receiving the SMS token, the user can enter the token.
    • If the login fails, the user stays on the same page and can try again.
    • When the TAN login was successful, the PatchMTanRestState AuthState is triggered. This AuthState changes the state of the used mTAN credential to "active" in nevisIDM.
      • The PatchMTanRestState AuthState requires the property parameter.baseUrl, which specifies the URL of the REST API of nevisIDM, to be able to send requests to the API.
    • After this step, the authentication using the mTAN credential of the user was successful.
<AuthState name="IdmMultipleMTanLoginRest" class="ch.nevis.esauth.auth.states.tan.TANState" authLevel="auth.weak" final="false" resumeState="true" >
<ResultCond name="ok" next="PatchMTanRestState"/>
<Response value="AUTH_CONTINUE">
<Gui name="MTANDialog">
<GuiElem name="lasterror" type="error" label="${notes.lasterrorinfo}" value="${notes.lasterror}"/>
<GuiElem name="isiwebotp" type="text" label="SMS token"/>
<GuiElem name="submit" type="submit" label="login.button.label"/>
<GuiElem name="entryid" type="hidden" label="" value="${inctx:connection.HttpHeader.Host}"/>
</Gui>
</Response>
<property name="channel" value="SwissPhone"/>
<property name="sender" value="__sender-phone-number__"/>
<property name="recipient" value="${session:ch.nevis.idm.mtan.credential.selected.mobile}"/>
<property name="response" value="${inargs:isiwebotp}"/>
<property name="generateNewTan" value="false"/>
<property name="maxRegenerate" value="0"/>
<property name="maxRetry" value="3"/>
<property name="tanTemplate" value="5{0123456789}"/>
<property name="username" value="__username__"/>
<property name="password" value="__password__"/>
<property name="portalListServerAddress" value="__portal-address__"/>
</AuthState>
<AuthState name="PatchMTanRestState" class="ch.nevis.esauth.auth.states.scripting.ScriptState" final="false">
<ResultCond name="ok" next="IdmPostProcessing"/>
<property name="parameter.baseUrl" value="https://__idm-hostname__:__port__/nevisidm/api" />
<property name="script" value="file:///var/opt/nevisauth/default/conf/patch-mTan-credential-state.groovy"/>
</AuthState>
import ch.nevis.idm.client.IdmRestClient
import ch.nevis.idm.client.IdmRestClientFactory

String url = parameters.get('baseUrl') + "/" + "core/v1/" + session.get('ch.adnovum.nevisidm.clientId') + "/users/" + session.get('ch.adnovum.nevisidm.userExtId') + "/mtans/" + session.get('ch.nevis.idm.mtan.credential.selected.extId');
IdmRestClient idmRestClient = new IdmRestClientFactory().getInstance();

try {
idmRestClient.patch(url, '{ "stateName": "active" }');
response.setResult('ok');
}
catch(Exception e) {
LOG.error(e.getMessage());
}
  • In case of a loginWithRecoveryCode transition, that is, when the user selected the recovery code option, the LoginWithRecoveryCode AuthState is called. This AuthState uses the login-with-recovery-code.groovy script.
    • The user can enter one of their recovery codes.
    • If the login fails, the user stays on the same page. The "Login failed." error message is displayed, and the user can try again.
    • When the user manages to log in with a recovery code, then the authentication with the recovery code credentials of the user was successful.
    • The LoginWithRecoveryCode AuthState requires the property parameter.baseUrl, which specifies the URL of the REST API of nevisIDM, to be able to send requests to the API.
<AuthState name="LoginWithRecoveryCode" class="ch.nevis.esauth.auth.states.scripting.ScriptState" authLevel="auth.strong" final="false">
<ResultCond name="ok" next="IdmPostProcessing"/>
<ResultCond name="failed" next="LoginWithRecoveryCode"/>
<Response value="AUTH_CONTINUE">
<Gui name="enterRecoveryCode" label="Enter one of your recovery codes">
<GuiElem name="restError" type="error" label="${notes.restError}" value="${notes.restError}"/>
<GuiElem name="recoveryCode" type="pw-text" label="Recovery code"/>
<GuiElem name="submit" type="button" label="Login" value="continue"/>
</Gui>
</Response>
<property name="parameter.baseUrl" value="https://__idm-hostname__:__port__/nevisidm/api" />
<property name="script" value="file:///var/opt/nevisauth/default/conf/login-with-recovery-code.groovy"/>
</AuthState>
import ch.nevis.idm.client.IdmRestClient
import ch.nevis.idm.client.IdmRestClientFactory
import ch.nevis.idm.client.IdmRestClientException

if (inargs.getProperty('submit') == 'continue' && inargs.getProperty('recoveryCode')) {
def recoveryCode = inargs.getProperty('recoveryCode');
String url = parameters.get('baseUrl') + '/' + 'auth/v1/' + session.get('ch.adnovum.nevisidm.clientId') + '/users/' + session.get('ch.adnovum.nevisidm.userExtId') + '/recovery-codes/login/';
IdmRestClient idmRestClient = new IdmRestClientFactory().getInstance();
try {
idmRestClient.post(url, '{"code": "' + recoveryCode + '" }');
response.setResult('ok');
}
catch (IdmRestClientException loginFailedException) {
LOG.error(loginFailedException.getMessage());
notes.put('restError', "Login failed.");
response.setResult('failed');
}
}
  • After a successful authentication, the final stage of the Account Recovery Flow is reached. This stage actually consists of two steps.
    • In the first step, the user can select one of their profiles (if the user has more than one), by means of the IdmPostProcessing AuthState.
    • After this, the authentication is marked as "done", and the session is created. You can redirect your users wherever you want using the AuthGeneric AuthState. A good practice is to redirect your users to a page where they can immediately change their credentials (for example, add the new phone). The path or URL where you want to redirect the users can be defined in the nevis.transfer.destination argument of the AuthGeneric AuthState.
    • If you want to redirect your users to a custom-built UI based on the nevisIDM REST API, see the separate integration guide for more information.
<AuthState name="IdmPostProcessing" class="ch.nevis.idm.authstate.IdmGetPropertiesState" final="false">
<ResultCond name="ok" next="AuthRedirect"/>
<ResultCond name="showGui" next="IdmPostProcessing"/>
<ResultCond name="default" next="AuthDone"/>
<ResultCond name="SOAP:showGui" next="AuthDone"/>
<ResultCond name="SOAP:default" next="AuthDone"/>
<Response value="AUTH_CONTINUE">
<Gui name="AuthError">
<GuiElem name="lasterror" type="error" label="${notes.lasterrorinfo}" value="${notes.lasterror}"/>
</Gui>
</Response>
</AuthState>

<AuthState name="AuthRedirect" class="ch.nevis.esauth.auth.states.standard.AuthGeneric" final="true" >
<Response value="AUTH_DONE">
<Arg name="nevis.transfer.type" value="redirect"/>
<Arg name="nevis.transfer.destination" value="__URL_OR_PATH_OF_CLIENT_APPLICATION__"/>
</Response>
</AuthState>