[ServiceNow] Utah新機能 - MFAの二要素認証でメール認証利用時の初回認証アプリ設定を回避する

# はじめに

ServiceNowのUtahバージョンから、Multi-factor Authentication （MFA） の機能が拡張され、二要素目の認証として、Software token (Authenticator app) だけでなく、UserのE-mail宛にOnetime Password (OTP) を送ることも選択できるようになりました。
しかし、ServiceNowの仕様では、初回の認証においては必ずSoftware tokenとの紐づけが求められ、2回目以降の認証でないとメール送信が選択できません。
ここでは、この仕様を回避して、Software tokenとの紐づけを必要としないカスタマイズの例を紹介します。

:::note alert
このカスタマイズは、ServiceNowのOOTBのログインセキュリティ仕様を変更するものです。
変更は、自己責任で行ってください。
:::

# 初期設定

まずは、OOTBのMFAが動作するように設定します。

1. Multi-factor Authentication pluginをインストールします。
    1. [参照] https://docs.servicenow.com/bundle/utah-platform-security/page/integrate/authentication/task/t_ActivateMultifactorAuthenticator.html 
1. Multi-factor criteriaを設定します。Role-basedとUser basedがあり、特定のRoleが付与されているユーザーにMFAを適用する方法と、ユーザー単位にMFAを適用する方法に分かれています。Role-basedの場合、デフォルトでadmin、user-admin、itilが指定されているので、これらを削除して、専用のRoleを作成して、指定したほうが良いかもしれません。
    1. [参照] https://docs.servicenow.com/bundle/utah-platform-security/page/integrate/authentication/task/t_RequireMultifactorAuthForAUser.html
    1. [参照] https://docs.servicenow.com/bundle/utah-platform-security/page/integrate/authentication/task/mfa-role-criteria.html
1. MFAを有効にします。All > Multi-factor Authentication > Propertiesを開き、Enable Multi-factor authenticationをtrueにします。このとき、併せて、Enable email OTP for Multi-factor authenticationがtrueになっていることを確認します。
    1. [参照] https://docs.servicenow.com/bundle/utah-platform-security/page/integrate/authentication/reference/mfa-properties.html

# カスタマイズ

初回ログイン時に、手動でSoftware tokenがServiceNowインスタンスに紐づけることなく、正常に完了したと誤認させるため、All > Multi-factor Authentication > User Multi-factor Setupからから開けるUser Multifactor Authentication (user_multifactor_auth) テーブルのValidated (is_validated)フィールドのDefault Valueをtrueにします。

![Validated_Default_True.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490802/eae2c872-d770-2341-c6a3-e1f3cbc61995.png)

次に、初回ログイン時にSoftware tokenが誤認された形で紐づいたときに、そのログインセッションを強制的に切るために、Multifactor Authentication (user_multifactor_auth) テーブルに新規Buriness Ruleを作成します。強制的にセッションを切らないと、インスタンスURLにアクセスしても、Software tokenの紐づけを促す画面に強制的にリダイレクトされ、User nameとPasswordを入力する通常のログイン画面に遷移できないためです。

![User_MFA_BR1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490802/844b5c8e-8168-ee08-8ea1-1513523ab260.png)
![User_MFA_BR2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490802/e07be699-baab-d640-e43e-c4c93ecab459.png)

```JavaScript:Script
(function executeRule(current, previous /*null when async*/ ) {

    // Add your code here
    gs.log("SessionLockOutOnInitialLogin runs for: " + current.user.user_name);
    GlideSessions.lockOutSessionsInAllNodes(current.user.user_name);
})(current, previous);

```

最後に、初回ログイン時にSoftware tokenを紐付けために、OOTBの画面で表示されるQRコードと6 digits OTPの入力フィールドをを無効化して、適切な案内に変更します。
All > System UI > UI Pagesを開き、OOTBのUI Pageであるmulti_factor_auth_setup_pageのHTMLをカスタマイズします。

![UIPage_MFA_Setup.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490802/9ad36a79-7b0d-8965-f695-0371052c32d5.png)

```HTML:HTML
<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
    <link href="84f0346b87120300cfab6dd207cb0b72.cssdbx?" rel="stylesheet" type="text/css"/>
	<g:evaluate jelly="true">  
      var title = gs.getMessage('Enable multi-factor authentication');
      gs.getSession().getHttpSession().setAttribute("isPartialSession", "true");
      var totp = new GlideRecord('user_multifactor_auth');
      totp.addQuery('user', gs.getUserID());
      totp.addQuery('is_validated', true);
      totp.query();        
      totp;
   </g:evaluate>

    <g:ui_form>
        <div data-form-title="$[title]"></div>
        <input type="hidden" id="mfa_setup_completed" name="mfa_setup_completed" value="${totp.hasNext()}" />

        <j:choose>
            <!-- Polarisberg -->
            <j:when test="${sn_ui.PolarisUI.canUsePolarisTemplates()}">
                <div class="login-wrapper">
                    <g:inline template="polarisberg_login_wrapper_svg.xml"/>

                    <div class="login-container wide">
                        <div class="login-logo">
                            <g:inline template="polarisberg_login_logo.xml"/>
                        </div>
                        <div class="login-card ${jvar_hide_illustrations}">
                            <g:inline template="polarisberg_login_card_svg.xml"/>

                            <div class="login-form">
								<!-- Customization from here: Display the alternative contents to bypass a software token -->  
                                <h1>${gs.getMessage('Login setting initialized')}</h1>
								<ol>
									<li>
										${gs.getMessage('Your login setting is successfully initialized.')}
									</li>
									<li>
										${gs.getMessage('Reload the page from your browser button.')}
									</li>
									<li>
										<a href="login.do">${gs.getMessage('Alternatively, access the login page again.')}</a>
									</li>
								</ol>
								<!-- Customization to here: Display the alternative contents to bypass a software token -->  
                                
								<!-- Customization from here: Hide the original contents by the comment out -->
								<!--
								<div class="notification notification-failure" id="divNotification" style="display:none;">
                                    <button data-dismiss="alert" class="btn-icon close icon-cross">
                                        <span class="sr-only">Close</span>
                                    </button>
                                </div>

                                <h1>${gs.getMessage('Enable multi-factor authentication (MFA)')}</h1>
                                <h2><a href="https://docs.servicenow.com/search?q=CSHelp:MFA-Authenticator" target="_blank">${gs.getMessage('More information')}</a></h2>
								-->

                                <!-- <a href="https://docs.servicenow.com/search?q=CSHelp:multifactor-authentication" target="_blank" id="linkLearnMore">${gs.getMessage('Learn more')}</a> -->

                                <!-- Customization from here: Hide the original contents by the comment out -->
								<!--
								<j:if test="${!totp.hasNext()}">
                                    <ol>
                                        <li>
                                            ${gs.getMessage('Download an authenticator app that supports Time Based One-Time Password (TOTP) on your mobile device.')}
                                        </li>
                                        <li>
                                            ${gs.getMessage('Open the app and scan the QR code below to pair your mobile device')}
                                        </li>
                                    </ol>

                                    <div class="login-form-field">
                                        <img id="imgCode" tabindex="0" alt="${gs.getMessage('Scan this QR code to pair your mobile device')}" />
                                    </div>
                                    <div class="login-form-field">
                                        <label tabindex="0">${gs.getMessage('Or enter this code in your app:')}</label>
                                        <div class="mfa-setup-code" tabindex="0" title="">
                                            <div id="lblCode"></div>
                                            <span id="copy_code" aria-label="${gs.getMessage('Click to copy code')}" data-original-title="${gs.getMessage('Click to copy code')}" data-toggle="tooltip" class="btn btn-default sn-tooltip-basic icon icon-copy" title="" onclick="copyCodeToClipboard()" />
                                        </div>
                                        <label id="cod_cop_msg" style="color: rgb(31, 132, 118); margin-top: 5px; display: none;" tabindex="0"></label>
                                    </div>

                                    <ol class="mfa-setup-step-3" start="3">
                                        <li>
                                            ${gs.getMessage('Enter the code generated by the Authenticator app below')}
                                        </li>
                                    </ol>

                                    <div class="login-form-field">
                                        <label tabindex="0">${gs.getMessage('6-digit verification code')}</label>
                                        <input id="txtResponse" class="form-control" type="text" placeholder="${gs.getMessage('XXX - XXX')}" aria-label="${gs.getMessage('Enter the 6 digit code generated by the authenticator app')}" name="txtResponse" autocomplete="off"/>
                                    </div>

                                    <button class="btn btn-primary"  data-toggle="modal" id="btnValidate" onclick="return validateResponse('${jvar_form_id}');">${gs.getMessage('Pair device and Login')}</button>
                                </j:if>
								-->
								
                                <j:if test="${totp.hasNext()}">
                                      <div id="tdSuccessDownload">
                                          <p tabindex="0">${gs.getMessage('You have successfully configured the authenticator app in your mobile device. If you have lost your device, please go to your user profile section to pair a new device.')}</p>
                                      </div>
                                </j:if>
                            </div>
                            <div class="login-card-footer text-center">
                                <a id="linkBypassSetup" class="btn btn-link" onclick="bypassSetup();" style="display:none;" href="#">
                                    ${gs.getMessage('Postpone setup')}
                                </a>
                                <p id="remainingBypassCountPara" style="display:none;">${gs.getMessage('Number of times MFA setup can be postponed is: ')}<span id="remainingBypassCount"></span></p>
                                <input type="hidden" name="hdnBypassSetup" id="hdnBypassSetup" />
                            </div>
                        </div>
                    </div>
                </div>
            </j:when>
            <!-- Heisenberg -->
            <j:otherwise>
                <g:requires name="styles/heisenberg/heisenberg_all.css" includes="true" />

                <div class="notification notification-failure" id="divNotification" style="display:none;">
                    <button data-dismiss="alert" class="btn-icon close icon-cross">
                        <span class="sr-only">Close</span>
                    </button>
                </div>

                <div class="ga-outer-container">
                    <div class="ga-header-container">
                        <h1 class="ga-label-header-large" tabindex="0">${gs.getMessage('Enable multi-factor authentication(MFA)')}</h1>
                        <a href="https://docs.servicenow.com/search?q=CSHelp:multifactor-authentication" target="_blank" id="linkLearnMore">${gs.getMessage('Learn more')}</a>
                        <a id="linkBypassSetup" onclick="bypassSetup();" style="display:none; padding-left: 1em;" href="#">${gs.getMessage('Postpone Setup')}</a>
                        <input type="hidden" name="hdnBypassSetup" id="hdnBypassSetup" />

                        <p class="ga-label" id="remainingBypassCountPara" style="display:none;">${gs.getMessage('Number of times MFA setup can be postponed is: ')}<span id="remainingBypassCount"></span></p>

                    </div>
                    <j:if test="${!totp.hasNext()}">
                    <div class="ga-flex-container" >
                        <div class="ga-flex-content">
						<p class="ga-label" tabindex="0">
							<g:no_escape>${ALLOW_JELLY:gs.getMessage("mfa_setup_step1")}</g:no_escape>
						</p>
                        </div>
                        <div class="ga-flex-content ga-flex-content-center">
						<label class="ga-label" tabindex="0">${gs.getMessage('mfa_setup_step2')}</label>			
                            <img id="imgCode" tabindex="0" alt="${gs.getMessage('Scan this QR code to pair your mobile device')}" />
						<label class="ga-label" tabindex="0" style=" text-align: center;">Or type in</label>
						<div style="float: right;">
							<div style="width: 100%;" tabindex="0" title="">
								<div id="lblCode" style="font-size: small;display: inline;padding-right: 20px;"></div>
								<span id="copy_code" aria-label="${gs.getMessage('Click to copy code')}" data-original-title="${gs.getMessage('Click to copy code')}" data-toggle="tooltip" class="btn btn-default sn-tooltip-basic icon icon-copy" title="" onclick="copyCodeToClipboard()" />
							</div>
						</div>
						<label class="ga-label" id="cod_cop_msg" style="text-align: center; color: rgb(31, 132, 118); margin-top: 5px; display: none;" tabindex="0"></label>
                        </div>

                        <div class="ga-flex-content">
						<label class="ga-label" tabindex="0">${gs.getMessage('mfa_setup_step3')}</label>
                            <span class="modal-footer flex">
                                <input id="txtResponse" class="col-sm-9 form-control" type="text" placeholder="${gs.getMessage('6-digit code')}" aria-label="${gs.getMessage('Enter the 6 digit code generated by the authenticator app')}" name="txtResponse" autocomplete="off"/>
                            </span>
                            <span class="modal-footer flex">
                                <button class="btn btn-primary"  data-toggle="modal" id="btnValidate" onclick="return validateResponse('${jvar_form_id}');">${gs.getMessage('Pair device and Login')}</button>
                            </span>
                        </div>
                    </div>
                        </j:if>
                </div>
            
            
                <j:if test="${totp.hasNext()}">
                    <div class="ga-outer-container">
                        <div class="ga-header-container">
                            <div class="ga-flex-content" id="tdSuccessDownload">
                                <p class="ga-label" tabindex="0">${gs.getMessage('You have successfully configured the authenticator app in your mobile device. If you have lost your device, please go to your user profile section to pair a new device.')} </p>
                            </div>
                        </div>
                    </div>
                </j:if>
            </j:otherwise>
        </j:choose>
    </g:ui_form>
</j:jelly>
```

# ログイン時の振る舞い

実際に、Userがどのようにログインできるか見てみます。ここでは、test.core.mfa.user01というTest Userを用意します。UserのEnable Multifactor Authenticationフィールドをtrueにして、User-basedでMFAを適用しています。
次のように、User nameとPasswordでログインします。

![MFA_User_Login.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490802/77f0b709-5bed-81fc-2d1d-8db2038ebec0.png)

ログインすると、カスタマイズした画面が表示されます。
このとき、ServiceNowインスタンスでは、Software tokenが自動的に誤認された形でひも付き、かつログインセッションが切られています。

![MFA_Setup.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490802/91815c2a-9895-f9c2-0162-2ee629298c26.png)

画面の案内に沿って、ログイン画面に遷移します。
User nameとPasswordで、再度ログインします。

![MFA_User_Login.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490802/77f0b709-5bed-81fc-2d1d-8db2038ebec0.png)

無事、Software tokenを紐付けることなく、Software tokenか、Email送信かを選択する画面に遷移しました。
このあとは、Email送信を選択して、通常通りダイアログを進めるだけで、UserのEmail宛にOTPを受け取り、それを入力することでログインに成功します。
なお、Software tokenを選択しても、OTPを入力する画面に遷移はしますが、実際に紐づいているTokenがないため、ログインすることはできません。この画面に戻ってくるしかありません。
ここで、Software token選択肢をなくせることが理想ですが、この画面はServiceNowインスタンスに編集できない形で埋め込まれているため、現時点ではカスタマイズできないようです。

![Verify_Your_Identity.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490802/abaa5ab6-b3a2-3e16-3224-a3a90be95024.png)