まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タグ:appid

IBM Cloud から提供されている IDaaS サービスである AppID を使っています。AWS の Cognito や Auth0 のようなユーザーログインの(オンラインサインアップやオンラインパスワード変更、多要素認証なども含めた)機能全般をまとめて提供するサービスです。IBM Cloud のライトアカウントを使って無料枠内で利用することも可能です。単なるログイン機能程度であれば自分で実装してもいいのですが、オンラインサインアップや多要素認証まで考慮すると実装は面倒だし、(実在しないアドレスを使うこともできますが)メールアドレス含めて管理するのは個人情報管理の観点からも、特に試験的/実験的な段階では可能であれば避けたいという運用側の事情もあるため、こういったマネージドサービスを使うメリットは大きいと考えています。事実、自分は公私でよく使っています。
2021082902


で、この AppID を使ってサービス開発をしている中で「ユーザーを無効化したい」という意見をいただきました。実験的な意味合いのあるサービスを作って、ある特定の期間だけはそのサービスを使ってもらい、その期間が完了した後は使わせたくない。でもユーザーを削除してしまうと、何かの際に再度ログインしてもらうこともできなくなったり、再作成しても ID のデータ紐付けが切れてしまってたまったデータとの関連が切れてしまうのでそれは都合が悪い。そのため「いったん無効化してログインできないようにしたい」という、運用上ごもっともな意見でした。

「ユーザーの無効化ね。まあ機能的に用意されてるんだろうな」と楽観視していたのですが、AppID のダッシュボードを調べてみると、どうも「ダッシュボード画面からはユーザーの削除はできるが無効化はできなそう」でした。おっと、これはまずいぞ。。。
2021082903


このユーザー無効化を実現するためのワークアラウンドとして考えたのが「パスワードの強制変更」でした。要は管理者の権限で該当ユーザーのパスワードを強制的に変更することで事実上ログインできなくしてしまう、という方法です。アカウントは残るのでデータやその紐付けも消えることはなく、再度初期パスワードに変更すれば、万が一の時にもう一度ログインしてもらうこともできるようになる、と考えました。

で、じゃあダッシュボードからどのようにパスワードを強制変更するのかというと・・・実はパスワードの強制変更もダッシュボードからできるわけではありません。2021年8月現在、AppID ダッシュボードからはユーザーを削除することはできますが、ユーザーを無効化することも、パスワードを強制変更することもできません。

ただ、AppID SDK にはパスワードを強制変更する API がありました(文字通りの「無効化」は SDK からでもできなさそう・・):
https://www.npmjs.com/package/ibmcloud-appid

2021082901
(↑このページ内の "Set User New Password" 欄参照)


つまり AppID SDK のこの setUserNewPassword 関数を使って自分でプログラムを作れば、ユーザーのパスワードを強制的に変更すること自体は実現できそうでした。それによって実質的な無効化も可能にできるのでは・・と考え、挑戦してみました。以下はその成果の共有です。


【サンプルソースコードの紹介】
AppID のユーザーパスワードを変更するサンプルの Node.js アプリケーションを作って、ソースコードを公開しました:
https://github.com/dotnsf/appid_changepassword

このリポジトリを git clone するかダウンロード&展開してください。

使い方は後述しますが、まずは依存ライブラリをまとめてインストールしておいてください(この手順はソースコードを用意した後に1回だけ行ってください):
$ cd appid_changepassword

$ npm install

このフォルダには4つの JavaScript コードファイルが含まれていますが、それぞれ以下のような役割です(create_users_to_appid.js とか、何気に便利なツールだと自負してます):
ファイル役割
app.jsApp ID の動作確認用ウェブアプリケーション。AppID にログイン&ログアウトするだけ
settings.js動作設定用ファイル
create_users_to_appid.jsCSV ファイルから AppID のユーザーをまとめて作成する CLI アプリ
change_password.jsユーザーIDと新パスワードを指定して、パスワードを強制変更する CLI アプリ


パスワードの変更処理を行うのは change_password.js ですが、app.js や create_users_to_appid.js も含めて、この3つのファイルを動かすためにはあらかじめ settings.js に AppID の接続情報を記載しておく必要があります。以下、一通りの使い方を紹介します。


【サンプルソースコードを動かすための事前設定項目】
このアプリを動かすには、何はともあれ App ID のインスタンスが必要です。IBM Cloud にログインしてダッシュボードから App ID を選択してリソースインスタンスを作成してください。その際に「ライト」プランを選択すると 1000 ユーザー&(一ヶ月)1000 ログインまで無料で利用することが可能です(ログインアカウントがライトアカウントの場合は、ライトプランしか選択できません):
2021082904


作成した AppID インスタンスの「サービス資格情報」から(必要であれば1つ作成した上で)サービス資格情報を選択して、資格情報の内容を確認します:
2021082905


資格情報の中は以下のような JSON フォーマットのテキストになっています:
{
  "apikey": "xxxxx",
  "clientId": "xxxxxxxxxxxxxxxxxxx",
  "secret": "xxxxxxxxxxxxxxxxxxx",
  "tenantId": "xxxxx",
  "profilesUrl": "https://jp-tok.appid.cloud.ibm.com",
    :
    :
}

この中の以下5つの情報が必要です:
・apikey
・secret
・clientId
・tenantId
・region(profilesUrl の "https://" と ".appid.cloud.ibm.com" の間の文字列(上例だと "jp-tok"))

これら5つの値を見つけたら、settings.js 内の該当する変数値として入力し、保存します:
//. IBM App ID
exports.region = '(region の値)';
exports.tenantId = '(tenantId の値)';
exports.apiKey = '(apikey の値)';
exports.secret = '(secret の値)';
exports.clientId = '(clientId の値)';

exports.redirectUri = 'http://localhost:8080/appid/callback';
exports.oauthServerUrl = 'https://' + exports.region + '.appid.cloud.ibm.com/oauth/v4/' + exports.tenantId;

準備の最後に App ID のコールバック URL を登録します。今回のアプリケーションはローカルホストで動かす想定をしていて、その場合はローカルホストで動かす際の URL を事前に AppID に登録しておく必要があります。

IBM Cloud ダッシュボードで作成した App ID を開き、「認証の管理」、「認証設定」を選択して、「Web リダイレクト URL の追加」から "http://localhost:8080/appid/callback" を追加します(ローカルホストで動かすのではなく、公開アドレスで動かす場合はその URL を使って "http(s)://ホスト名/appid/callback" を追加してください):
2021082906


これでアプリケーションを動かす準備ができました。


【サンプルソースコードを使ってユーザーを登録】
このブログエントリの目的は「AppID のユーザーパスワードを変更する方法の紹介」です。そのため実際にログインできるユーザーを使って動作確認する必要がありますが、まずはそのユーザーを登録する必要があり、そのためのツールを紹介します。 既に AppID にユーザーが登録されていて、そのユーザーのパスワードを変更してもいい場合、ここは無視しても構いません。

まずは登録するユーザーを以下のようなフォーマットの CSV ファイルで用意します(newusers.csv 参照):
ユーザー表示名,ユーザーID,ユーザーパスワード
ユーザー表示名,ユーザーID,ユーザーパスワード
    :
    :


1列目のユーザー表示名はログインしたユーザーをアプリケーション内で表示する際に表示される文字列です。AppID の場合、ここはなぜか「8文字以上」という制約があるので、8文字以上の表示名を指定します。

2列目のユーザーIDは「メールアドレス形式のログインID」です。App ID の設定によってはオンラインでパスワードリセットを行ったりすることも可能で、その場合のメール送信先にもなるものです(オンラインパスワードリセットを無効にして運用する場合は実在しないメールアドレスでも構いません)。

3列目のユーザーパスワードは、2列目の ID でログインするユーザーのログイン時に指定するパスワードです。 この3列を一組とした行を必要なユーザーぶんだけ用意します。 なおサンプルで newusers.csv というファイルが含まれていて、その内容は以下のような2行になっています(以下、このファイルを使って紹介しますが、自分でこのファイルに相当する内容のファイルを用意した場合は、そのファイル名に置き換えて読み進めてください):
User0001,user0001@mydomain.com,password1
User0002,user0002@mydomain.com,password2

ではこのファイルを使って AppID にユーザーを登録します。コマンドの最後に CSV ファイル名を指定して、以下のように実行します:
$ node create_users_to_appid newusers.csv

画面に色々出力された後にプロンプトに戻ります。成功していると CSV ファイル内で指定されたユーザーが AppID に登録されているはずです。ダッシュボードから「クラウド・ディレクトリー」、「ユーザー」を選択し、CSV ファイルで指定したユーザーが追加されていることを確認します:
2021082907


検証用のユーザーが AppID に登録できました。まずはこのユーザー ID(と CSV ファイルで指定したパスワード)でログインできるか一度確認しておきます。


【サンプルソースコードを使って登録したユーザーでログイン確認】
では今登録したユーザーが登録したパスワードでログインできるかどうかを確認するため、サンプルのウェブアプリケーションを起動します:
$ node app

App ID のメッセージが数行表示された後に "server starting on 8080 ..." と表示されれば起動完了です。ウェブブラウザを開いて http://localhost:8080/ にアクセスします。すると(ログイン前なので) AppID のログイン画面に転送され、以下のような画面が表示されます:
2021082901


この Email 欄と Password 欄にそれぞれユーザーのメールアドレスとパスワードを入力して "Sign in" ボタンをクリックします。先程 CSV ファイルで作成したユーザーのメールアドレスとパスワード(例えば user0001@mydomain.com と password1)を入力してみます:
2021082902


正しく入力されている状態で "Sign in" ボタンをクリックするとログイン処理が成功し、画面が遷移して以下のような画面に切り替わります。ユーザーの ID、表示名、メールアドレスが表示されています。アドレスはログイン前に指定したものと同じ http://localhost:8080/ ですが、ログインが完了しているのでログイン画面には転送されず、この画面が表示されています。"Logout" ボタンでログアウトできます:
2021082903


ログアウトするとログイン画面に戻ります。試しにもう1人のユーザー情報(ID: user0002@mydomain.com, パスワード: password2)でもログインしてみます:
2021082904


User0002 でもログインできました。CSV ファイルから作成したユーザーで正しくログインできる所まで確認できました:
2021082905


ここまで確認できたらいったんサンプルウェブアプリケーションを終了しておきます。"node app" を実行した画面で Ctrl+C を押してアプリケーションを強制終了させておきます。


【サンプルソースコードを使ってユーザーのパスワードを変更する】
さていよいよ本ブログエントリの本題となる「ユーザーパスワードの強制変更」の箇所です。change_password.js ファイルを使って以下のように実行します:
$ node change_password user0001@mydomain.com Password1

実行時に指定するパラメータは2つあります。1つ目がパスワードを変更したいユーザーのログインID(メールアドレス)、2つ目が変更後のパスワードです(つまり上の例だと user0001@mydomain.com ユーザーのパスワードを Password1 に変更します)。旧パスワードを指定せずに強制的に変更する、という点に注意してください。実行すると色々画面に表示されますが、これも処理が完了するとプロンプトに戻ります。

パスワード変更が正しく行われたかどうかを再度サンプルウェブアプリケーションで確認します:
$ node app

ウェブブラウザで https://localhost:8080/ にアクセスしてログイン画面に移動したら、最初は変更前のパスワード(password1)を指定して user0001@mydomain.com でログインを試みてください。先程はログインできましたが、今はパスワードが変更されているのでログインできないはずです(メールアドレスとパスワードが一致しない、という旨のエラーメッセージが表示されます):
2021082906


改めて変更後のパスワードを指定してログインすると、今度はログインが成功してログイン後の画面が表示されます:
2021082907


これで AppID SDK を使ってログインパスワードを強制的に変更することができることが確認できました。

ちなみにソースコード上でのこのパスワード変更処理箇所はこのようになっています:
var SelfServiceManager = require( 'ibmcloud-appid' ).SelfServiceManager;

  :
  :

selfServiceManager.setUserNewPassword( uuid, password, "en", null, null ).then( function( user ){
  resolve( user );
}).catch( function( err ){
  reject( err );
});

  :
  :


ibmcloud-appid ライブラリを呼び出して SelfServiceManager をインスタンス化し、(省略した部分で)アクセストークンを取得してメールアドレスで指定したユーザーの ID(上記コード上では uuid)を特定します。特定できたら selfServiceManager.setUserNewPassword( uuid, password, "en", null, null ); を実行してパスワードを変更しています(password が新しいパスワード)。3番目のパラメータは言語情報らしいですが現在使われていないらしく、既定値の "en" を指定しています。このあたり、詳しくは SDK の説明もご覧ください。


ともあれ、これで当初の目的である「App ID ユーザーの無効化」に近いオペレーションが実現できそうでした。めでたしめでたし。



IBM Cloud から提供されているユーザーディレクトリ(ログイン機能用サービス)である App ID で、デモ用途などでまとめて一括でユーザーを作成したいことはないでしょうか? 1件や2件程度であればサービスダッシュボードから直接作成してもいいのですが、10件とか100件とかになると面倒ですよね。

というわけで「指定した CSV ファイルに記載された情報を使って App ID のユーザーを API 経由で作成するツール」を作って公開してみました。Node.js 環境があれば実行可能です:
https://github.com/dotnsf/appid_users


上記リポジトリを git clone するかダウンロード&展開すると test.csv という CSV ファイルが見つかります。このフォーマットに従う形で、
 表示名,メールアドレス,パスワード
という順に値が1行ずつ記録された CSV ファイルを用意します(メールアドレス=ログインIDになります)。

次に settings.js ファイルを編集して、利用する App ID のサービス接続情報に書き換えます。

準備の最後に依存ライブラリをインストールします:
$ npm install


そして create_user.js を Node.js で実行すると、この CSV ファイルに記述されたユーザーをまとめて作成します(最後に対象 CSV ファイルを指定します):
$ node create_user test.csv

この作業後に App ID のコンソールからクラウド・ディレクトリーのユーザー一覧を確認すると、CSV ファイルで指定したユーザーが作成されていることを確認できます:
2021071001


作成後にログイン確認用のウェブアプリケーションを実行して、ユーザーが正しく作成されているかどうかを確認することができます。その場合はリダイレクト URL に http://localhost:8080/appid/callback を追加しておいてください:
2021071003


改めてアプリケーション起動後($ node app)に http://localhost:8080/ にアクセスし、CSV ファイルに含まれていた ID とパスワードを指定して、正しくログインできるかどうかを確認してください。ログインできるようになっていれば無事に CSV ファイルからユーザーをインポートすることができた、ことになります:
$ node app


2021071002



IBM Cloud から提供されているユーザーディレクトリ管理サービスである App ID の UI カスタマイズに挑戦しています。自分的にはまだ道半ばではありますが、途中経過という意味でいったん公開・紹介します。


【IBM Cloud AppID とは?】
IBM Cloud App ID サービス(以下「App ID」)はアプリケーションで利用するユーザーディレクトリを管理するサービスです。オンラインサインアップを含めたユーザー管理機能を持ち、アプリケーションにログイン機能を付加させたい場合に非常に簡単にログイン機能を実装できるようになります。IBM Cloud の(無料の)ライトアカウントを持っていれば(2要素認証など、一部機能が使えなかったり、1日の実行回数などに制限もありますが)無料のライトプランで利用することも可能です:
2021052900



【IBM Cloud AppID のカスタマイズに挑戦した背景】
App ID はユーザー管理機能が必要なアプリケーション開発を行う上で非常に便利な一方、UIのカスタマイズに関する情報が少なく(管理メニューに用意されているのは、ログイン画面内にロゴ画像を貼り付けることができる程度)、ほぼあらかじめ用意された(英語メインの)画面を利用する必要がありました。

機能的には満足なのですが、この UI カスタマイズがどの程度厄介なものなのかを調べる意味も含めて、AppID の UI カスタマイズに挑戦してみました。結論として 2021/06/02 のこのブログエントリ公開時点では 100% の実装ができているわけではないのですが、ログイン画面およびパスワードリセット画面のカスタマイズには成功しているため、ここでいったん公開することにしました。


【IBM Cloud AppID のカスタマイズサンプル】
AppID UI カスタマイズを使ったサンプルアプリケーションのソースコードはこちらで公開しています:
https://github.com/dotnsf/appid_fullcustom


サンプルアプリケーションを利用するには IBM Cloud にログインして、App ID サービスのインスタンスを作成し、サービス資格情報を作成・参照する必要があります:
2021060201


そして以下の値を確認します:
{
  "apikey": "*****",
  "appidServiceEndpoint": "https://us-south.appid.cloud.ibm.com",
  "clientId": "*****",
  "secret": "*****",
  "tenantId": "*****",
    :
    :
}

これらの値をソースコード内 settings.js のそれぞれの変数に代入して保存します:
//. IBM App ID
exports.region = 'us-south';
exports.tenantId = '*****';
exports.apiKey = '*****';
exports.secret = '*****';
exports.clientId = '*****';

exports.redirectUri = 'http://localhost:8080/appid/callback';
exports.oauthServerUrl = 'https://' + exports.region + '.appid.cloud.ibm.com/oauth/v4/' + exports.tenantId;


※exports.region の値は appidServiceEndpoint の値の "https://" と ".appid.cloud.ibm.com" の間の文字列です。それ以外はサービス資格情報にかかれている値をそのまま記載します。

また exports.redirectUri の値はアプリケーションの OAuth ログインのリダイレクト先 URL を記載します。このサンプルアプリケーションでは 8080 番ポートで待ち受けて /appid/callback にリダイレクトする想定で作られているのでこのような値になりますが、実際にパブリックなインターネットで利用する場合はこの値を実際の URL 値に書き換えてください。

準備の最後に AppID サービスのリダイレクト URI にここで指定した export.redirectUri の値と同じものを登録します。サービスの「認証の管理」メニューから「認証設定」タブを選択し、「Web リダイレクト URL の登録」欄に export.redirectUri に指定した値と同じものを追加してください。これで準備は完了です:
2021060202


このサンプルアプリケーションを実際に動かしてみましょう。まずは npm install して node app で起動します:
$ cd appid_fullcustom

$ npm install

$ node app

8080 番ポートで待ち受ける形で起動するので、ウェブブラウザで http://localhost:8080/ にアクセスします。正しく動作すると http://localhost:8080/login にリダイレクトされ、以下のようなシンプルなログイン画面になります(この画面はカスタムUIで作った画面です):
2021060203


AppID サービスに登録した ID とパスワードを入力して "Login" します:
2021060204


ログインに成功すると、そのユーザーの名前などが表示される画面になります。この画面で "logout" すると元のログイン画面に戻ります:
2021060205


ログイン画面下部に2つのリンクがあります。「オンラインサインアップ」と書かれたリンクをクリックすると、オンラインサインアップ用の画面に切り替わります(この画面もカスタム UI です):
2021060201


ここでメールアドレスなどを入力してサインアップ可能です:
2021060202


サインアップしてしばらく待つと、メールアドレスの有効性確認を行う必要があるため、指定したメールアドレスにメールが届きます(なお、自分の環境では「迷惑メール」扱いで届きました(苦笑))。メールを開いてリンクをクリックし、有効性確認処理をしてください:
2021060203


メールアドレスの有効性が確認されるとオンラインサインアップが完了したとみなされ、これ以降はメールアドレスとサインアップ時に指定したパスワードでログインできるようになります:
2021060203


ログイン画面下部のもう1つのリンク「パスワードを忘れた場合」はパスワードリセット用のリンクです:
2021060204


こちらをクリックするとパスワードを忘れてしまったメールアドレスを指定する画面(これもカスタムUIです)が表示されるので、ここにパスワードを忘れたアカウントのメールアドレスを指定します:
2021060205


すると指定したメールアドレスにパスワードリセット用の URL が書かれたメールが届きます。このメールを開いて、URL にアクセスします:
2021060206


すると新しいパスワードを入力する画面(この画面だけはカスタムUIではなく、あらかじめ用意されたものです)が表示されるので、新しいパスワードを入力します:
2021060207


これで新しいパスワードを使って再びログインが可能になります:
2021060203


念の為、カスタマイズ無しの場合の AppID の各種画面UIを以下に紹介しておきます。(Your Logo Here) と書かれた箇所にロゴ画像を貼り付けることは可能ですが、それ以外のカスタマイズをしようとすると今回紹介したサンプルのような方法を取る必要があります。その代わり、これらの画面を使う場合は非常に簡単に実装することができるものです。

(ログイン画面)
2021060301

(パスワード忘れ)
2021060302

(オンラインサインアップ)
2021060303



【まとめ】
以上、AppID を独自のカスタム UI で利用する手順を紹介しました。一連の手順の中でパスワードリセット時の新パスワードを指定する画面だけは元の(英語の)UIになってしまっていますが、それ以外はすべて独自の UI で実現できているのがおわかりいただけると思います。この残った箇所の対応は前後関係含めて対応する必要があってちと面倒そうな印象も持っているので、いったんこの状況で公開しました。細かな実装についてはソースコードを参照ください。

便利な AppID を好みの UI で使えるメリットは大きいと思っていますので、興味ある方の参考になれば。


IBM Cloud から提供されているサービスの1つである AppID は、オンラインサインアップやパスワード忘れなどにも対応したログイン機能全般を簡単にアプリに組み込める形で提供されているサービスです。

自分も何度か使ったことがあります。Node.js 向けに提供されている SDK を使うと、サービスの属性値や認証後のコールバック URL をいくつか設定するだけで、あらかじめ用意された UI を使った認証機能を独自アプリケーション内に組み込んだり、認証後のユーザー情報を簡単に取得することができます。多くのアプリケーションではユーザー情報やユーザー認証が必要になるのですが、いちいちそのアプリケーション向けに開発する手間が省けるため、とても有用に活用しています。


ただ、唯一の難点だったのが「あらかじめ用意された UI」でした。カスタマイズできる要素が非常に少なく(ロゴ画像を1つ指定できるくらい)、アプリケーションのUIを頑張って作っても「ログイン時だけ他のページと違うテイストのUI」になってしまうことが気になっていました。

このUI画面もカスタマイズできるらしい、という噂は聞いていたのですが、軽く調査した限りでは具体的な情報がなくわからず、つまりある程度自分でトライ&エラーしながら調べる必要がありそうな内容だったのですが、ちゃんと調べる時間が取れずに放っておいてしまいました。

この度、自分の興味が再度高まったこともあって、改めてこの App ID UI のカスタマイズについて調べてみました。どうやら実現できそうな目処が付いたので、本ブログエントリの形でその方法を紹介したいと思います。なお以下は Node.js + Express というフレームワーク環境を前提としていますが、SDK の対応言語であればおそらく他の言語やフレームワークでも大丈夫だと思います。


【AppID の準備】
何はともあれ AppID サービスインスタンスを用意して、アプリケーション開発用の属性情報を取得する必要があります。まずは開発前の AppID 側の準備作業を紹介します。

IBM Cloud にログインし、「リソースの作成」から "App ID" を検索して、見つけたらクリックします:
2021030304


ロケーション(東京とか)とプラン(ライトであれば2段階認証が使えないなどの制約がありますが無料です)を選択して「作成」します:
2021030305


少し待つとインスタンスが作成され、App ID が利用可能な状態になります:
2021030306


作成したこの App ID を外部から利用するための属性情報を取得します。「サービス資格情報」タブを選択し、「新規資格情報」ボタンをクリックして、資格情報を1つ作成します。作成後に資格名(「サービス資格情報-1」などになっているはずです)をクリックすると、資格情報が JSON フォーマットで表示されます:
2021030303


資格情報は概ねこのような内容になっています(***** 部は伏せ字)。このうち、以下5つの値は後で利用するのでメモしておくか、コピペできるようにしておきます:
{
  "apikey": "********************",
  "appidServiceEndpoint": "********************",
  "clientId": "********************",
  "discoveryEndpoint": "********************",
  "iam_apikey_description": "Auto-generated for key ********************",
  "iam_apikey_name": "サービス資格情報-1",
  "iam_role_crn": "crn:v1:bluemix:public:iam::::serviceRole:Manager",
  "iam_serviceid_crn": "crn:v1:bluemix:public:iam-identity::a/********************::serviceid:ServiceId-********************",
  "managementUrl": "https://jp-tok.appid.cloud.ibm.com/management/v4/********************",
  "oauthServerUrl": "https://jp-tok.appid.cloud.ibm.com/oauth/v4/********************",
  "profilesUrl": "https://jp-tok.appid.cloud.ibm.com",
  "secret": "********************",
  "tenantId": "********************"
}


後で使う値確認方法
regionprofilesUrl などのURL文字列の .appid.cloud.ibm.com の前に付いている部分(上の例だと jp-tok)
tenantIdtenantId キーの値
apiKeyapikey キーの値
secretsecret キーの値
clientIdclientId キーの値


次に AppID 自体を利用可能な状態にしておきます。まずログイン時に利用するにはユーザーが登録されている必要があるので、あらかじめユーザーを登録しておきます。

※ AppID 自体がオンラインサインアップの機能を持っているのでオンラインサインアップでユーザー登録することも可能ですが、その説明は今回のブログの目的ではないのと、ここで説明する内容を完結にしたい目的もあってここでは紹介しません。今回はオンラインサインアップや外部 SNS アカウントを用いたログイン、パスワード忘れ時の対応等、ログインに無関係な機能を OFF にして説明します。もちろんこれらの機能もカスタム UI で同様に提供することは可能ですが、今回の説明では対象外とします。

AppID のダッシュボード画面から「クラウド・ディレクトリー」-「ユーザー」を選択し、「ユーザーの作成」ボタンをクリックします:
2021030303


名前(名と姓、表示用)、Eメール(ログインIDとなります)、パスワードを入力して「保存」します。なおオンラインサインアップでアカウントを作成した場合はメール認証が必要ですが、この直接作成する方法でユーザーを作成した場合は保存後すぐに有効なアカウントとして(メール認証なしに)登録されます:
2021030304


ユーザーが登録されました。(今回の検証では1つあれば充分ですが)必要に応じて同じ手順でユーザーを追加してください:
2021030305


最後に App ID 自体の挙動を構成します。今回はオンラインサインアップやパスワード忘れ時の対応などをすべて無効にして、登録済みユーザーのログイン機能だけを提供するように構成します。

最初に認証時のリダイレクト URL を指定します。実はこのリダイレクト URL の指定が必要なのはカスタマイズしない標準のログイン UI を使う場合のみで、独自 UI を使う際には指定の必要がないものですが、今回は比較のためカスタマイズしない場合でのログインも行うのでここでリダイレクト URL も設定しておきます。

ダッシュボード画面から「認証の管理」を選択し、「認証設定」タブ内の「Web リダイレクト URL の追加」からリダイレクト先を追加します。今回のデモでは localhost で動かすことを想定したリダイレクト先が http://localhost:8080/appid/callback となるサンプルを利用するので、この URL を指定して追加します:
2021030306


次に不要な機能を無効にします。まずは SNS サインイン機能を無効にします。ダッシュボード画面で「認証の管理」から「ID プロバイダー」を選び、「クラウド・ディレクトリー」だけを有効にします(他をすべて無効にします):
2021030308


最後にダッシュボードの「設定」を選択し、「ユーザーがアプリケーションをサインアップできるようにする」、「ユーザーがアプリケーションからアカウントを設定できるようにする」を両方とも「いいえ」に設定します:
2021030307


これで App ID 側の準備はすべて完了しました。ではこの App ID を使ったログイン機能を試してみましょう。


【カスタマイズなしの場合】
本エントリは AppID の UI をカスタマイズする方法を紹介することが主目的ですが、比較の意味も含めて、まずはカスタマイズせずに標準UIを使ってログインする場合のアプリケーションサンプルを紹介します。

サンプルはこちらに用意しました。実際に試す場合は Node.js を導入したシステムにダウンロードして展開するか、git clone するなどしてソースコードを手元に用意してください:
https://github.com/dotnsf/appid_customui

2021030309


ソースコードの準備ができたら、展開後のディレクトリで一度 npm install を実行して、依存ライブラリをまとめて導入しておきます:
$ cd appid_customui
$ npm install

そしてテキストエディタで settings.js を開き、2~6行目までの5箇所に上述の資格情報から取得した値を入力します。8行目の exports.redirectUri の値は既定値以外のコールバック URL を設定した場合のみ、その値に変更してください。変更が完了したら保存します:
//. IBM App ID
exports.region = '(上述の region の値)';
exports.tenantId = '(上述の tenantId の値)';
exports.apiKey = '(上述の apikey の値)';
exports.secret = '(上述の secret の値)';
exports.clientId = '(上述の clientId の値)';

exports.redirectUri = 'http://localhost:8080/appid/callback';
exports.oauthServerUrl = 'https://' + exports.region + '.appid.cloud.ibm.com/oauth/v4/' + exports.tenantId;

コードの説明をする前に一度実際に動かして挙動を確認してみましょう。以下のコマンドでアプリケーションを起動します:
$ node app

App ID の初期化などで少し時間がかかりますが、10 秒ほどすると "server starting on 8080 ..." というメッセージが表示され、起動が完了します。このメッセージを確認したらウェブブラウザで以下のアドレスを開きます:
http://localhost:8080/

アプリケーションが正しく起動していれば、App ID が提供するログインページに転送され、以下のような標準のログイン画面が表示されます(ロゴを加える程度のカスタマイズは可能ですが、ちと味気ないですよね。。):
2021030303


ID とパスワードに、先程 App ID のユーザー登録を行った際のメールアドレス及びパスワードを指定して "Sign in" ボタンをクリックします:
2021030304


正しいメールアドレスとパスワードの組み合わせが入力されていればログインが成功します。ログインに成功すると改めて http://localhost:8080/ に転送され、ログインしたユーザーの名前とログアウトボタンだけが表示される画面に遷移します。これがログイン後のトップページとなります(この画面は、もうちょっと見た目がんばることができると思ってはいますが、、自分で自由にデザインできます):
2021030305


このサンプルではトップページ以外に遷移するページはありません。"Logout" ボタンをクリックするとログアウトして再度 http://localhost:8080/ へ遷移します。が、今度はログインしていないので再び標準ログイン画面のページに転送されます。ここまでが App ID 標準ログイン画面を使ったデモの挙動です:
2021030303


ここまでの(標準ログイン画面を使ったログインの)挙動をコードでどのように実現しているかを説明しておきます。まず node コマンドで起動した app.js の全容はこのようになっています:
//. app.js
var express = require( 'express' ),
    bodyParser = require( 'body-parser' ),
    ejs = require( 'ejs' ),
    passport = require( 'passport' ),
    session = require( 'express-session' ),
    WebAppStrategy = require( 'ibmcloud-appid' ).WebAppStrategy,
    app = express();

var settings = require( './settings' );

//. setup session
app.use( session({
  secret: 'appid_normalui',
  resave: false,
  saveUninitialized: false
}));

app.use( bodyParser.urlencoded( { extended: true } ) );
app.use( bodyParser.json() );
app.use( express.Router() );
app.use( express.static( __dirname + '/public' ) );

app.set( 'views', __dirname + '/views' );
app.set( 'view engine', 'ejs' );


//. setup passport
app.use( passport.initialize() );
app.use( passport.session() );
passport.use( new WebAppStrategy({
  tenantId: settings.tenantId,
  clientId: settings.clientId,
  secret: settings.secret,
  oauthServerUrl: settings.oauthServerUrl,
  redirectUri: settings.redirectUri
}));
passport.serializeUser( ( user, cb ) => cb( null, user ) );
passport.deserializeUser( ( user, cb ) => cb( null, user ) );


//. login UI(**カスタムログインページでは変更)
app.get( '/login', passport.authenticate( WebAppStrategy.STRATEGY_NAME, {
  successRedirect: '/',
  forceLogin: true,
}));

//. logout
app.get( '/appid/logout', function( req, res ){
  WebAppStrategy.logout( req );
  res.redirect( '/' );
});

//. callback(*カスタムログインページでは不要)
app.get( '/appid/callback', function( req, res, next ){
  next();
}, 
  passport.authenticate( WebAppStrategy.STRATEGY_NAME )
);

//. ログイン済みでないとトップページが見れないようにする
app.all( '/*', function( req, res, next ){
  if( !req.user || !req.user.sub ){
    //. ログイン済みでない場合は強制的にログインページへ
    res.redirect( '/login' );
  }else{
    next();
  }
});

//. トップページ
app.get( '/', function( req, res ){
  //. 正しくユーザー情報が取得できていれば、トップページでユーザー情報を表示する
  if( req.user ){
    res.render( 'index', { user: req.user } );
  }else{
    res.render( 'index', { user: null } );
  }
});


var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

情報量としてはあまり多くありませんが、ログイン後の画面となる views/index.ejs の内容も載せておきます。Logout ボタンをクリックすると GET /appid/logout が実行されるようになっていることと、その下にログインユーザーの名前(user.name)が表示されるテンプレートになっています:
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8"/>
<script type="text/javascript" src="//code.jquery.com/jquery-2.2.4.min.js"></script>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/4.3.0/css/bootstrap.min.css" rel="stylesheet"/>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/4.3.0/js/bootstrap.min.js"></script>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<meta name="apple-mobile-web-app-title" content="App ID Custom UI"/>
<title>App ID Custom UI</title>
<script>
</script>
<style type="text/css">
</style> 
</head>
<body>

<div class="container">
  <div>
    <a class="btn btn-warning" href="/appid/logout">Logout</a>
  </div>

  <% if( user && user.name ){ %>
  <h2><%= user.name %></h2>
  <% }else{ %>
  <h2>(App ID)</h2>
  <% } %>
</div>

</body>
</html>


具体的な詳しい内容はこのコードを見ていただくとして、この中には以下の4つの HTTP リクエストに対するルーティング(と1つのルール)が含まれています:
#HTTP リクエスト挙動
1GET /loginApp ID標準ログイン画面の表示。ログインが成功すると GET / (#5)の処理へ転送されます。
2GET /appid/logoutログアウト処理。その後 GET / (#5) を実行しようとするが、この時点ではログインしていないので、#4 のルールによって GET /login (#1) へ転送されて標準ログイン画面を表示します。
3GET /appid/callback標準ログイン画面のコールバック関数。正しいログイン組み合わせの情報が送信された場合は next()(処理続行)となり、間違った組み合わせの場合は再度ログイン画面に戻ります。
4ALL /*(ここから下のルーティングすべてに適用するルール)ログインされていない場合は強制的に GET /login (#1) へリダイレクトして標準ログイン画面に移動します。
5GET /index.ejs を使ってトップ画面を表示する。#4 のルールがあるのでログイン後の場合のみ表示します。


先程確認した挙動をもう一度このリストを使って順に説明します。まずブラウザで http://localhost:8080/ にアクセスすると #5 のルーティングが処理される、、ように見えますが、その前に #4 が定義されています。最初はログインしていない状態で実行されるため、まずは GET /login( #1 の処理)へ転送され、App ID の標準ログイン画面が表示されます。

ログイン画面で正しい情報を入力して Sign in ボタンを押すと、ログイン済みになった上で #3 へ移動し、処理が続行(#1)され 、ログイン済みなので改めて #5 (http://localhost:8080/)へ転送されます。

最初と同じ #5 のルーティングが処理されますが、今回はログイン済みのため #4 のルールは適用されません。そのまま #5 の処理が実施され、ログイン後の画面が表示されることになります。この後のオペレーションはすべて「ログイン済み」な状態として行われることになります:
2021030404


ログイン後のトップ画面で logout ボタンを押すと #2 が実行され、ログイン情報が消えた状態になってから再度 http://localhost:8080/ へ転送され(つまり最初に戻り)、標準のログイン画面が表示されます。


ちょっと長くなりましたが、これが App ID の標準ログイン画面を使った時のログイン~ログアウトまでのルーティングとその遷移となります:



【UI をカスタマイズする場合】
標準ログイン画面を使った場合の状態遷移がわかった所で、改めてログイン画面の独自カスタマイズについて紹介します。こちらもまずは一度動かしてみることにしましょう。以下のコマンドでアプリケーションを起動します(先程は app.js を起動しましたが、こちらでは app_customlogin.js を起動します):
$ node app_customlogin

App ID の初期化などで少し時間がかかりますが、10 秒ほどすると "server starting on 8080 ..." というメッセージが表示され、起動が完了します。このメッセージを確認したらウェブブラウザで以下のアドレスを開きます:
http://localhost:8080/

アプリケーションが正しく起動していれば、先程のような App ID 標準ログイン画面とは異なる、以下のような独自のログイン画面が表示されます:
2021030303


App ID にユーザー登録した際の username(メールアドレス)と password(パスワード)の組み合わせを入力して "Login" ボタンをクリックします:
2021030304


正しい組み合わせが入力できていれば、ログイン後の画面(名前が表示されている画面)に遷移します。そして "Logout" ボタンをクリックします:
2021030305


するとログアウトが行われ、あらためて元のカスタマイズされたログイン画面に遷移します:
2021030303


カスタマイズされたログイン画面を使う場合であっても、App ID に登録されたユーザー情報を用いて標準ログイン画面の時と同様の挙動や状態遷移が行われることを確認できました。

ではこちらのコードを確認してみます。起動した app_customlogin.js の内容は以下のようになっています:
//. app_customlogin.js
var express = require( 'express' ),
    bodyParser = require( 'body-parser' ),
    ejs = require( 'ejs' ),
    passport = require( 'passport' ),
    session = require( 'express-session' ),
    WebAppStrategy = require( 'ibmcloud-appid' ).WebAppStrategy,
    app = express();

var settings = require( './settings' );

//. setup session
app.use( session({
  secret: 'appid_customui',
  resave: false,
  saveUninitialized: false
}));

app.use( bodyParser.urlencoded( { extended: true } ) );
app.use( bodyParser.json() );
app.use( express.Router() );
app.use( express.static( __dirname + '/public' ) );

app.set( 'views', __dirname + '/views' );
app.set( 'view engine', 'ejs' );


//. setup passport
app.use( passport.initialize() );
app.use( passport.session() );
passport.use( new WebAppStrategy({
  tenantId: settings.tenantId,
  clientId: settings.clientId,
  secret: settings.secret,
  oauthServerUrl: settings.oauthServerUrl,
  redirectUri: settings.redirectUri
}));
passport.serializeUser( ( user, cb ) => cb( null, user ) );
passport.deserializeUser( ( user, cb ) => cb( null, user ) );


//. login UI(**カスタムログインページで変更)
app.get( '/login', function( req, res ){
  res.render( 'login', {} );
});

//. logout
app.get( '/appid/logout', function( req, res ){
  WebAppStrategy.logout( req );
  res.redirect( '/login' );
});

//. login submit(***カスタムログインページで追加)
app.post( '/appid/login/submit', bodyParser.urlencoded({extended: false}), passport.authenticate(WebAppStrategy.STRATEGY_NAME, {
  successRedirect: '/',
  failureRedirect: '/login',
  failureFlash : true
}));

//. ログイン済みでないとトップページが見れないようにする
app.all( '/*', function( req, res, next ){
  if( !req.user || !req.user.sub ){
    //. ログイン済みでない場合は強制的にログインページへ
    res.redirect( '/login' );
  }else{
    next();
  }
});

//. トップページ
app.get( '/', function( req, res ){
  //. 正しくユーザー情報が取得できていれば、トップページでユーザー情報を表示する
  if( req.user ){
    res.render( 'index', { user: req.user } );
  }else{
    res.render( 'index', { user: null } );
  }
});


var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

↑わざと色を付けています。は app.js から変更した部分、は app.js にはなくて追加した部分です。また app.get( '/appid/callback', function( req, res, next ){ ... で始まるコールバック処理の部分は、カスタムログイン画面を使う場合は不要なので削除しています。実はほぼ変わっていなくて、削除した部分も含めて大きく3箇所しか変わっていないことがわかると思います。

加えて、カスタムログイン画面となる views/login.ejs の内容もここに記載しておきます。index.ejs 同様、Bootstrap を使っていますが、ごくシンプルな内容の HTML になっています。特徴として入力した username と password の値が上で新たに追加した POST /appid/login/submit にポストするように作られていることがわかります:
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8"/>
<script type="text/javascript" src="//code.jquery.com/jquery-2.2.4.min.js"></script>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/4.3.0/css/bootstrap.min.css" rel="stylesheet"/>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/4.3.0/js/bootstrap.min.js"></script>
<link href="//stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<meta name="apple-mobile-web-app-title" content="App ID Custom UI"/>
<title>App ID Custom UI - Login</title>

<style type="text/css">
</style> 

<script>
</script>

</head>
<body>

<div class="container">
  <form method="POST" action="/appid/login/submit">
    <table class="table table-bordered">
      <tbody>
        <tr><th>username</th><td><input type="text" class="form-control" name="username" id="username" value=""/></td></tr>
        <tr><th>password</th><td><input type="password" class="form-control" name="password" id="password" value=""/></td></tr>
      </tbody>
    </table>
    <input type="submit" class="btn btn-primary" value="Login"/>
  </form>
</div>

</body>
</html>


このカスタムログイン画面を使うケースでも、HTTP リクエストとルーティングの関係を表にしてみました:
#HTTP リクエスト挙動
1GET /loginカスタムログイン画面の表示。ログインが成功すると GET / (#5)の処理へ転送されます。
2GET /appid/logoutログアウト処理。その後 GET / (#5) を実行しようとするが、この時点ではログインしていないので、#4 のルールによって GET /login (#1) へ転送されてカスタムログイン画面を表示します。
3POST /appid/login/submitカスタムログイン画面で username と password を入力して Login ボタンを押した時に実行される関数。正しいログイン組み合わせの情報が送信された場合は GET / (#5) が実行されてトップ画面へ、間違った組み合わせの場合は GET /login (#1) が実行され、再度ログイン画面に戻ります。
4ALL /*(ここから下のルーティングすべてに適用するルール)ログインされていない場合は強制的に GET /login (#1) へリダイレクトしてカスタムログイン画面に移動します。
5GET /index.ejs を使ってトップ画面を表示する。#4 のルールがあるのでログイン後の場合のみ表示します。


まずブラウザで http://localhost:8080/ にアクセスすると App ID の標準ログイン画面を使った時と同様に(最初はログインしていない状態で実行されるため)まずは GET /login( #1 の処理)へ転送されます。その結果、カスタムログイン画面が表示されます。

ログイン画面で正しい情報を入力して Sign in ボタンを押すと、#3 の POST /appid/login/submit が実行されます。情報が正しい場合、ログイン済みになった上で #5 へ移動します。今度はログイン済みなのでログイン後の画面が表示されることになります。この後のオペレーションはすべて「ログイン済み」な状態として行われることになります:


2021030404


というわけで、英語でもあまり情報の見つかられなかった App ID のカスタムログインページ作成方法についてまとめてみました。


なお、本ブログエントリと、ここで紹介したサンプルを作る上で参考にしたのはこの Github リポジトリです。使い方の説明が丁寧にかかれているわけではないのですが、カスタムログインページだけでなく、パスワード忘れ時の対応画面など、一通りの App ID カスタム画面を開発するためのサンプルが含まれている(と思います):
https://github.com/ibm-cloud-security/appid-serversdk-nodejs


このページのトップヘ