はじめに
ServiceNowのUtahバージョンから、Multi-factor Authentication (MFA) の機能が拡張され、二要素目の認証として、Software token (Authenticator app) だけでなく、UserのE-mail宛にOnetime Password (OTP) を送ることも選択できるようになりました。
しかし、ServiceNowの仕様では、初回の認証においては必ずSoftware tokenとの紐づけが求められ、2回目以降の認証でないとメール送信が選択できません。
ここでは、この仕様を回避して、Software tokenとの紐づけを必要としないカスタマイズの例を紹介します。
このカスタマイズは、ServiceNowのOOTBのログインセキュリティ仕様を変更するものです。
変更は、自己責任で行ってください。
初期設定
まずは、OOTBのMFAが動作するように設定します。
- Multi-factor Authentication pluginをインストールします。
- Multi-factor criteriaを設定します。Role-basedとUser basedがあり、特定のRoleが付与されているユーザーにMFAを適用する方法と、ユーザー単位にMFAを適用する方法に分かれています。Role-basedの場合、デフォルトでadmin、user-admin、itilが指定されているので、これらを削除して、専用のRoleを作成して、指定したほうが良いかもしれません。
- MFAを有効にします。All > Multi-factor Authentication > Propertiesを開き、Enable Multi-factor authenticationをtrueにします。このとき、併せて、Enable email OTP for Multi-factor authenticationがtrueになっていることを確認します。
カスタマイズ
初回ログイン時に、手動でSoftware tokenがServiceNowインスタンスに紐づけることなく、正常に完了したと誤認させるため、All > Multi-factor Authentication > User Multi-factor Setupからから開けるUser Multifactor Authentication (user_multifactor_auth) テーブルのValidated (is_validated)フィールドのDefault Valueをtrueにします。
次に、初回ログイン時にSoftware tokenが誤認された形で紐づいたときに、そのログインセッションを強制的に切るために、Multifactor Authentication (user_multifactor_auth) テーブルに新規Buriness Ruleを作成します。強制的にセッションを切らないと、インスタンスURLにアクセスしても、Software tokenの紐づけを促す画面に強制的にリダイレクトされ、User nameとPasswordを入力する通常のログイン画面に遷移できないためです。
(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をカスタマイズします。
<?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でログインします。
ログインすると、カスタマイズした画面が表示されます。
このとき、ServiceNowインスタンスでは、Software tokenが自動的に誤認された形でひも付き、かつログインセッションが切られています。
画面の案内に沿って、ログイン画面に遷移します。
User nameとPasswordで、再度ログインします。
無事、Software tokenを紐付けることなく、Software tokenか、Email送信かを選択する画面に遷移しました。
このあとは、Email送信を選択して、通常通りダイアログを進めるだけで、UserのEmail宛にOTPを受け取り、それを入力することでログインに成功します。
なお、Software tokenを選択しても、OTPを入力する画面に遷移はしますが、実際に紐づいているTokenがないため、ログインすることはできません。この画面に戻ってくるしかありません。
ここで、Software token選択肢をなくせることが理想ですが、この画面はServiceNowインスタンスに編集できない形で埋め込まれているため、現時点ではカスタマイズできないようです。