はじめに
Webアプリケーションを作る時には必須になるアカウント管理とそれに伴うログイン/ログアウト処理ですが、実際に実装するとなるとセキュリティ事故を起こさないようかなり注意深く実装を行う必要があり、可能な限り既にある実装・サービスを利用したいところです。IBM CloudにおいてもApp IDというサービスが提供されており、今回はこれを利用して実装を行ってみます。基本的には公式ドキュメントに従って実装するだけですが、実際にCode Engineにデプロイするまでの手順を行うことで気づいた注意点などを記していこうと思います。
App IDを構成
まずはApp IDのインスタンスをprovisionします。こちらも無料枠があるので簡単に試す程度であれば費用はほとんど発生しないと思います。

認証処理を行う際、App IDの認可サーバーでユーザーがログインした後アプリケーションにリダイレクトされますが、その時のURLを設定する必要があります。App IDの管理画面の「認証の管理」→「認証設定」で以下のようなリダイレクトURLを設定します。
実際にApplicationのURLが決まったらここに追加する必要がありますが、まずはローカル環境のみ指定しておきます。
Node.jsでのApp IDの利用
Node.jsでApp IDを使うために以下のようなモジュールをインストールします。
$ npm install --save express express-session passport pug ibmcloud-appid
app.jsでは以下のように指定します。
const session = require('express-session'); // https://www.npmjs.com/package/express-session
const passport = require('passport'); // https://www.npmjs.com/package/passport
const WebAppStrategy = require('ibmcloud-appid').WebAppStrategy; // https://www.npmjs.com/package/ibmcloud-appid
app.use(session({
secret: '123456',
resave: true,
saveUninitialized: true
}));
app.use(passport.initialize());
app.use(passport.session());
passport.serializeUser((user, cb) => cb(null, user));
passport.deserializeUser((user, cb) => cb(null, user));
passport.use(new WebAppStrategy({
tenantId: "{tenant_ID}",
clientId: "{client_ID}",
secret: "{secret}",
oauthServerUrl: "{OAuth_Server_URL}",
redirectUri: "http://localhost:3000/auth/authenticate"
}));
このままではsessionの扱いに問題がありますが、今回は動作確認なのでこのままにしておきます。tenant_ID等の値はApp IDの「アプリケーション」のところにあるcredentialsから適宜コピーしておきます。
さらにログイン・ログアウト用のエンドポイントも定義しておきます。
app.get('/auth/login', passport.authenticate(WebAppStrategy.STRATEGY_NAME, {
successRedirect: '/',
forceLogin: true
}));
app.get('/auth/authenticate', passport.authenticate(WebAppStrategy.STRATEGY_NAME));
app.get('/auth/logout', (req, res) =>{
WebAppStrategy.logout(req);
res.redirect('/');
});
ブラウザで/auth/loginにアクセスするとApp IDのログイン画面にリダイレクトされ、そこでログイン情報を入力して認証されると/auth/authenticateにリダイレクトされ、ログインした状態でアプリケーションが利用できます。同様に/auth/logoutにアクセスするとログアウトしてからトップページにリダイレクトされることになります。
Dev serverにおけるproxy設定
前回の記事でclient/package.jsonにproxyの設定をしましたが、それはdev serverを使用しているときにtext/html以外のリクエストが来た場合にproxyに指定したURLに引き渡すという設定でした。しかし、上記の設定をしたexpressサーバーに/auth/loginのようなリクエストを投げたくても、そのリクエストはtext/htmlなので(App IDが提供してくれるログイン画面なので)dev serverが受け取ってしまい、トップページが表示されてしまいます。試しにhttp://localhost:3000/auth/loginを指定するとトップページが表示されることがわかると思います。逆に直接expressサーバーの方(http://localhost:8080/auth/login)にアクセスするとログイン画面が表示されると思います。

なのでプロダクションビルドでは問題ないですが、開発中はReactアプリの方からはログインできないことになるのでかなり面倒です。この問題はproxyをpackage.jsonでの指定ではなくちゃんと構成してやることで解決します。詳細な手順等はこちらを参照してください。
まずはhttp-proxy-middlewareを導入します。clientディレクトリで以下を実行します。
$ npm install http-proxy-middleware --save
次にsrc/setupProxy.jsを作成します。
const {createProxyMiddleware} = require('http-proxy-middleware');
module.exports = function(app) {
app.use('/api', createProxyMiddleware({
target: 'http://localhost:8080',
changeOrigin: true
}));
app.use('/auth', createProxyMiddleware({
target: 'http://localhost:8080',
changeOrigin: true
}));
}
(今後のため、前回/usersでルーティングしていたAPIは/api以下に変更しました。)
この状態でdev serverを起動すると、/apiと/authに関してはproxyされexpressサーバーの方へリクエストが渡されます。試しに http://localhost:3000/auth/loginにアクセスすると先ほどと同様のログイン画面が表示されることがわかると思います。
APIでのユーザー情報の取得
ログインが正常に完了すると、req.user以下にログイン情報が渡されてきます。routes/users.jsを以下のように変更します。
router.get('/', function(req, res, next) {
res.json({
login: !!req.user,
given_name: req.user?.given_name
});
});
これを呼び出すclient/src/App.jsの方では、まずAPI呼び出しのところをjsonを受け取るように変更します。
const [users, setUsers] = useState('');
useEffect(() => {
fetch('/api')
.then((res) => res.json())
.then((json) => setUsers(json));
}, []);
メッセージの表示部分は以下のように変更します。
<p>
Hello {users.given_name || "Unknown User"}!
</p>
<p>
{users.login &&
<a className="App-link" href="/auth/logout">Logout</a> ||
<a className="App-link" href="/auth/login">Login</a>
}
</p>
SPAではこのやり方は良くないのかもしれませんが、App IDがログインページを用意してくれているので、とりあえずページ遷移する実装を採用しています。もっと良い方法があるかも知れませんので実装する際は自己責任でお願いします。
実行してみます。

ログイン前なのでまずはUnknown Userとして表示され、"Login"のリンクが表示されています。ログインを押すとApp IDのログインページが表示されます。ここでLogin with Googleか先ほどApp IDの管理画面で登録したユーザーかどちらかでログインすると(Login with Facebookはアプリケーションの登録が必要そうでした)、以下の画面に遷移するのがわかると思います。
"Logout"のリンクをクリックするとまた最初の画面に戻ります。
設定情報の構成
基本的には前回と同じ方法でCode Engineにデプロイ可能ですが、一つ気をつけなければいけないのはApp IDのcredentialsをGitHubにはpushできない点です。ですので開発環境ではローカルのファイルを参照し、クラウド環境ではソースコードではなく別のところから設定する必要があります。
いくつかやり方はありますが、今回はnode-config(モジュール名はconfig)を使います。node-configは環境変数(NODE_ENV)に従って階層的にファイルから設定情報を読んでくれるモジュールです。公式ドキュメントによると以下のような順番で読み込まれます。
default.EXT
default-{instance}.EXT
{deployment}.EXT
{deployment}-{instance}.EXT
{short_hostname}.EXT
{short_hostname}-{instance}.EXT
{short_hostname}-{deployment}.EXT
{short_hostname}-{deployment}-{instance}.EXT
{full_hostname}.EXT
{full_hostname}-{instance}.EXT
{full_hostname}-{deployment}.EXT
{full_hostname}-{deployment}-{instance}.EXT
local.EXT
local-{instance}.EXT
local-{deployment}.EXT
local-{deployment}-{instance}.EXT
(Finally, custom environment variables can override all files)
{deployment}の部分がNODE_ENVで指定される部分になります。これを利用して、以下のファイル群を作成します。
config/default.json
config/production.json
config/local.json
config/custom-environment-variables.json
ここでlocal*はGitHubにpushしないものなので.gitignoreに追加しておきます。
/config/local*
ファイルの中身は以下のようにします。まずdefault.jsonには以降のファイルで指定しなかった場合に使われるデフォルトの値を指定します。ここでは開発環境のurlだけを指定しておきます。
{
"appid": {},
"url": "http://localhost:3000"
}
次にproduction.jsonにはクラウド環境で実行した時に使われる値を指定します。今回は特に記述できるものがないのでplaceholderのみです。
{
"appid": {}
}
local.jsonはGitHubにはpushしないファイルで、ローカル環境で参照する値を記述しておきます。このappidの中にはIBM Cloudコンソールからコピーしたcredentialsをそのままペーストしておきます。
{
"appid": {
"clientId": "...",
"tenantId": "...",
"secret": "...",
"name": "react-express",
"oAuthServerUrl": "https://...",
"profilesUrl": "https://...",
"discoveryEndpoint": "https://...",
"type": "regularwebapp",
"scopes": []
}
}
クラウド環境にデプロイしたあとは、環境変数を経由してcredentialsを読み込む必要があります。1つ1つ指定してもいいのですが、項目数が結構あるので、App IDのcredentialsのjsonを1つの環境変数を通じて読み込みます。custom-environment-variables.jsonを使うと環境変数のパースをして取り込むことができます。APPID_CREDENTIALSからjsonを読み込む指定方法は以下になります。また、アプリケーションのURLは単純にAPPLICATION_URLとして指定できるように記述します。
{
"appid": {
"__name": "APPID_CREDENTIALS",
"__format": "json"
},
"url": "APPLICATION_URL"
}
configを使ってアプリケーションを初期化するようにapp.jsを変更をします。
...
const config = require('config');
...
passport.use(new WebAppStrategy({
tenantId: config.get('appid.tenantId'),
clientId: config.get('appid.clientId'),
secret: config.get('appid.secret'),
oauthServerUrl: config.get('appid.oAuthServerUrl'),
redirectUri: `${config.get('url')}/auth/authenticate`
}));
(なぜかは不明ですが、WebAppStrategyに与えるプロパティ名はoauthServerUrlですが、jsonの方はoAuthServerUrlとなっていますので注意してください。)
この状態でローカルでログイン・ログアウトが可能なことを確認し、GitHubにもpushしておきます。
Code Engineへのデプロイ
では早速アプリケーションをデプロイしてみます。custom-environment-variables.jsonに指定したように、APPID_CREDENTIALSにApp ID用のcredentialsをセットする必要があります。Code Engineのプロジェクトの画面から、「シークレットおよびconfigmap」を選択し、シークレットを設定しておきます。

次にアプリケーションをデプロイします。前回と同様にリポジトリの指定等をしてから環境変数を指定します。

またリダイレクトURLを構築するためのAPPLICATION_URLも指定する必要があります。どのようなURLになるのか不明であれば、一度デプロイしてから構成の変更から指定してデプロイし直す方法もあります。デプロイが完了するのを待ってアプリケーションにアクセスし、ログイン・ログアウトが正常に完了することを確認します。
まとめ
IBM Cloud Code EngineでApp IDを使ったログイン・ログアウトの実装方法についてまとめました。App IDを使うと手間のかかるアカウント周りの実装を行う必要がないので便利にサービスを実装できます。
参考
ソースコード: https://github.com/fterui/react-express (次の記事用のコードも含みます)

