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

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

タグ:ui

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


システムプログラマーとして株式会社クーシーに入社後、半年が経ちました。

まだ毎日が勉強で、知らないことも驚くことも多いし、偉そうに感想を言えるような立場ではないのですが、特に大きく印象に残っている発見はこれです:
「今どきのウェブって、これだけの人がこれだけの作業をしてできているのか」

例えば企業のウェブサイト。中小企業だったりするとサイトの規模そのものは必ずしも大きいわけではないのです。極端な例ですが、1ページだけを作る場合で考えます。

これまでの自分がそのページを作ろうとすると、どちらかというと「やりたいことは技術的に実現できるかどうか」を中心に考えていました。今も自分の担当箇所でいえば技術的にできるかどうか、どのくらいでできそうか、ということが検討内容になりますが、それをどう見せるか、についてはあまり重視していませんでした。まあ「jQuery Mobileとか、Bootstrap とか、適当なフレームワークを適当にカスタマイズして使おう。画像は誰か描いて(苦笑)」程度に考えていた、というレベルです。ぶっちゃけ CSS とか基礎はわかっているつもりだけど自分ではサンプルを作る程度でそれ以上にいじることはほとんどない感じ。悪く言えば見た目は軽視してました。

今の会社では UI や UX を専門に担当するチームがあります。PC 用なのか、スマホ用なのか、レスポンシブデザインにするのかを考慮した上で、画像やグラフなどはピクセルレベルで調整して、必要であれば画像のデザインも行った上で HTML をデザインし、CSS を用意してくれます。僕らはその HTML をテンプレートにして組み込んだり、 CSS はロードして指定するだけ。これにバックエンド処理を加えたり、動的な JavaScript を加えたりしてページを作ります。良くも悪くも分業制を敷いて、それぞれのプロが担当する形です。クーシーの強みは(どちらかを外注するとかではなく)その両方のプロが所属しているところだと思います。

これまでの自分の感覚と比べると、軽視していた半分の作業を専門チームがやってくれている、という感じです。作業は細かいし、さすが質も高いし、そして自分は楽です(その代わり作業人月は増えるけど)。自分があまり注力していなかった分野のプロと接する機会がある、というだけでもすごくいい刺激が得られるし、技術を盗む相手という意味でも頼もしい。そして何よりも自分が担当したサイトやサービスがすごく良さげに見える(笑)。

と、そんな当たり前のことが新鮮な状態から自分にとっても当たり前のように感じつつある今日この頃です。


 

このページのトップヘ