はじめに
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 (次の記事用のコードも含みます)