IBM Cloud App IDを使ってCode Engineのアプリにログインを実装してみる

# はじめに

Webアプリケーションを作る時には必須になるアカウント管理とそれに伴うログイン／ログアウト処理ですが、実際に実装するとなるとセキュリティ事故を起こさないようかなり注意深く実装を行う必要があり、可能な限り既にある実装・サービスを利用したいところです。IBM CloudにおいてもApp IDというサービスが提供されており、今回はこれを利用して実装を行ってみます。基本的には[公式ドキュメント](https://cloud.ibm.com/docs/appid?topic=appid-web-node&locale=ja)に従って実装するだけですが、実際にCode Engineにデプロイするまでの手順を行うことで気づいた注意点などを記していこうと思います。


# App IDを構成
まずはApp IDのインスタンスをprovisionします。こちらも無料枠があるので簡単に試す程度であれば費用はほとんど発生しないと思います。
![スクリーンショット 2021-12-28 16.49.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/03d6504b-6b4c-d7ba-90d9-eec02cf901f8.png)

認証処理を行う際、App IDの認可サーバーでユーザーがログインした後アプリケーションにリダイレクトされますが、その時のURLを設定する必要があります。App IDの管理画面の「認証の管理」→「認証設定」で以下のようなリダイレクトURLを設定します。

![スクリーンショット 2021-12-28 20.40.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/0d9b45cd-29f7-1d35-6281-3e464b6914f0.png)

実際にApplicationのURLが決まったらここに追加する必要がありますが、まずはローカル環境のみ指定しておきます。

# Node.jsでのApp IDの利用

Node.jsでApp IDを使うために以下のようなモジュールをインストールします。

```
$ npm install --save express express-session passport pug ibmcloud-appid
```

`app.js`では以下のように指定します。

```javascript: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
```

```javascript:app.js(初期化部分)
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から適宜コピーしておきます。

さらにログイン・ログアウト用のエンドポイントも定義しておきます。

```javascript:app.js(エンドポイント)
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設定

[前回の記事](https://qiita.com/fterui/items/59d8d988bce4f78fff18)で`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`)にアクセスするとログイン画面が表示されると思います。
![スクリーンショット 2021-12-28 20.52.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/d15493c9-63b5-9372-bb51-59a5934584f2.png)

なのでプロダクションビルドでは問題ないですが、開発中はReactアプリの方からはログインできないことになるのでかなり面倒です。この問題はproxyを`package.json`での指定ではなくちゃんと構成してやることで解決します。詳細な手順等は[こちら](https://create-react-app.dev/docs/proxying-api-requests-in-development/#configuring-the-proxy-manually)を参照してください。

まずは`http-proxy-middleware`を導入します。`client`ディレクトリで以下を実行します。

```
$ npm install http-proxy-middleware --save
```
次に`src/setupProxy.js`を作成します。

```javascript: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`を以下のように変更します。

```javascript: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を受け取るように変更します。

```javascript:client/src/App.js
  const [users, setUsers] = useState('');
  useEffect(() => {
    fetch('/api')
      .then((res) => res.json())
      .then((json) => setUsers(json));
  }, []);
```

メッセージの表示部分は以下のように変更します。

```javascript:client/src/App.js
        <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がログインページを用意してくれているので、とりあえずページ遷移する実装を採用しています。もっと良い方法があるかも知れませんので実装する際は自己責任でお願いします。

実行してみます。
![スクリーンショット 2021-12-28 21.09.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/821396a5-d144-8e09-121e-4c15ca6ce1cc.png)
ログイン前なのでまずは`Unknown User`として表示され、"Login"のリンクが表示されています。ログインを押すとApp IDのログインページが表示されます。ここで`Login with Google`か先ほどApp IDの管理画面で登録したユーザーかどちらかでログインすると（`Login with Facebook`はアプリケーションの登録が必要そうでした）、以下の画面に遷移するのがわかると思います。

![スクリーンショット 2021-12-28 21.12.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/78f95e51-956d-b351-6e1d-290d137c9d57.png)

"Logout"のリンクをクリックするとまた最初の画面に戻ります。

# 設定情報の構成

基本的には前回と同じ方法でCode Engineにデプロイ可能ですが、一つ気をつけなければいけないのはApp IDのcredentialsをGitHubにはpushできない点です。ですので開発環境ではローカルのファイルを参照し、クラウド環境ではソースコードではなく別のところから設定する必要があります。

いくつかやり方はありますが、今回は[node-config](https://www.npmjs.com/package/config)(モジュール名は`config`)を使います。`node-config`は環境変数(`NODE_ENV`)に従って階層的にファイルから設定情報を読んでくれるモジュールです。[公式ドキュメント](https://github.com/lorenwest/node-config/wiki/Configuration-Files#file-load-order)によると以下のような順番で読み込まれます。

```
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`に追加しておきます。

```text:.gitignore
/config/local*
```

ファイルの中身は以下のようにします。まず`default.json`には以降のファイルで指定しなかった場合に使われるデフォルトの値を指定します。ここでは開発環境の`url`だけを指定しておきます。

```json:config/default.json
{
    "appid": {},
    "url": "http://localhost:3000"
}
```
次に`production.json`にはクラウド環境で実行した時に使われる値を指定します。今回は特に記述できるものがないのでplaceholderのみです。

```json:config/production.json
{
    "appid": {}
}
```
`local.json`はGitHubにはpushしないファイルで、ローカル環境で参照する値を記述しておきます。この`appid`の中にはIBM Cloudコンソールからコピーしたcredentialsをそのままペーストしておきます。

```json:config/local.json
{
    "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`として指定できるように記述します。

```json:config/custom-environment-variables.json
{
    "appid": {
        "__name": "APPID_CREDENTIALS",
        "__format": "json"
    },
    "url": "APPLICATION_URL"
}
```
`config`を使ってアプリケーションを初期化するように`app.js`を変更をします。

```javascript/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」を選択し、シークレットを設定しておきます。
![スクリーンショット 2022-01-03 11.22.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/b2bd22c0-538a-5b79-5a5f-d3343982a4e2.png)

次にアプリケーションをデプロイします。前回と同様にリポジトリの指定等をしてから環境変数を指定します。
![スクリーンショット 2022-01-03 11.30.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/2073c7d0-f51f-668b-4e86-9e55613bc446.png)

またリダイレクトURLを構築するための`APPLICATION_URL`も指定する必要があります。どのようなURLになるのか不明であれば、一度デプロイしてから構成の変更から指定してデプロイし直す方法もあります。デプロイが完了するのを待ってアプリケーションにアクセスし、ログイン・ログアウトが正常に完了することを確認します。

# まとめ
IBM Cloud Code EngineでApp IDを使ったログイン・ログアウトの実装方法についてまとめました。App IDを使うと手間のかかるアカウント周りの実装を行う必要がないので便利にサービスを実装できます。

# 参考

ソースコード: https://github.com/fterui/react-express (次の記事用のコードも含みます)