ベクトルタイルとAzure AD認証（nodejsでサーバーを作る）



## はじめに
自分のベクトルタイルを配信するときに、データにどんな認証をつけているでしょうか。

ここでは、私がベクトルタイルにAzure AD認証 をつけてみた経験を書きたいと思います。作業自体は少し前にやっていたのですが、作業記録として覚えていることを共有したいと思います。詳細を記録するというよりは方針や、参考になるコードの場所を記録するという感じになると思います。フィードバック、改善提案、ご助言があればお願いします！

なお、この作業は開発環境でやったものなので、まだ本物のインターネット上で公開しているサーバーには適用していません。


###なぜ認証が必要なのか
自分の作ったベクトルタイルをオープンに公開する場合には認証は必要ないと思います。一方で、不特定多数に公開したくないデータを含んでいたり、料金をとってサービスを提供したい場合などは、アクセスしてくる人を認証することが必要になります。私が扱っていたデータでも、同僚がデータの利用の際にはユーザーの認証が必要と考えていたので、ベクトルタイルの認証を試しました。

###既存サービスにおけるベクトルタイルのアクセス制限など
作業を進める前に、既存のサービスのアクセス制限がどうなっているか少しみてみました。間違いがあるかもしれません。

* ArcGSI Online
    * ArcGISサーバーでホストしているタイルのうち、一般に公開していないものを見てみました。ArcGISオンラインへのログイン後、ベクトルタイル地図を見ている様子を観察すると、ベクトルタイルのURLリクエストにクエリをつけて（?token=...）、アクセスを管理している様子。
* MapBox
   * AccessTokenとURLによる制約ということでしょうか。TokenはJSON Web Tokensを使っているようです。
      * https://docs.mapbox.com/help/getting-started/access-tokens/
      * https://docs.mapbox.com/help/troubleshooting/how-to-use-mapbox-securely/
* 国土地理院のベクトルタイル
    * pbfタイルで提供されていますが、パスワードや認証などは必要なさそうです。
* 英国Ordnance Surveyのベクトルタイル
    * アカウントがないのでわからないのですが、OS Data Hubの文書をみると OAuth 2を使っていそうです。
     * https://osdatahub.os.uk/docs/vts/overview


## 私の作業の条件
それぞれのベクトルタイルサーバーで状況が違うかと思いますが、私はnodejs/expressのサーバーからhttpsでベクトルタイルを配信しました。今回の作業の与条件となるのは主に以下の2点でした。

* nodejsでのサーバー構築が必要。これは、ベクトルデータのサイズが大きいためデータをmbtilesで格納しており、pbfタイルのリクエストに対して、nodejs/expressサーバーのルーティングと @mapbox/mbtiles というnpmモジュールで対応していたためです。[unvt/onyx](https://github.com/un-vector-tile-toolkit/onyx)の仕組みを使います。
* 認証はAzure AD認証をつかう。

## 環境
* サーバーの環境： 
    * RHEL　8.4
    * nodejs: ver12.21.0 (1年くらい前の作業で、mapbox/mbtilsの当時のバージョンが新しいnodejsで動かなかったため) 
* SSH/TLS：　httpsのための認証は内部用のものをもらった
* Azure AD Portalでの登録：ここは同僚が作業してくれて、テナントIDやシークレットをくれました。

## 手順
#### Step.0 Single Page​ Applicationのチュートリアルで少し慣れる（省略可能）
この分野の経験がないので、まずはAzure AD認証というか、msalパッケージをつかった仕組みに慣れてみました。（注：使うのはADAL（Azure Directory Authentication Library）でばなくMSAL（Microsoft Authentication Library））

マイクロソフトのチュートリアルはここにあります。
https://docs.microsoft.com/en-us/azure/active-directory/develop/tutorial-v2-javascript-spa
https://github.com/Azure-Samples/ms-identity-node

同僚に登録してもらった、クライアントID、authority、クライアントシークレットなどを使って内部環境で実験してみました。以下のような感じかなと理解しました。

（**このstepのここから下は半分以上は自分のメモみたいなので、不要なら読み飛ばしてください。**）

* msalのconfig変数（auth（クライアントID、オーソリティ、クライアントシークレット）とsystem（ログのオプション））を作る。
    * https://github.com/Azure-Samples/ms-identity-node/blob/main/index.js#L13-L28
* msal applicationオブジェクトの変数をつくる。前で作ったコンフィグ情報で、msalConfidentialClientApplication(作った変数)　として、msal applicationオブジェクトの変数に代入する。
    * https://github.com/Azure-Samples/ms-identity-node/blob/main/index.js#L31
* ルーティング。ルート（/）にきたリクエストを、{msal application オブジェクト}.getAuthCodeUrl(リダイレクトURLやスコープを持った変数)で得られるアドレスにリダイレクトする。（そのあとログインするとリダイレクトされると思われる。）
    * https://github.com/Azure-Samples/ms-identity-node/blob/main/index.js#L36-L46
* redirectのアドレスに帰ってきたら（帰ってくるときのrequestにcodeがついている）、codeでtokenRequestの変数をつくって、（msal application オブジェクト）.acquireTokenByCode(tokenRequest)として、トークンをとる。コードを使ってトークンをゲットしたということだろう。（サンプルコードではこのトークンをサーバー側で表示するようになっています。）
    * https://github.com/Azure-Samples/ms-identity-node/blob/main/index.js#L48-L62

すごく簡単にいうと、１）AuthCodeURLでAuthCodeをもらってそこでログインして、2)リダイレクトされたらそこからAuthCodeを使ってTokenを取得する、ということでしょうか。

OAuthの説明などを調べると、idTokenとAccessTokenは別物ですが、最初にコードとidTokenが帰ってきて、それをつかって、accessTokenとidTokenを取得するというようなイメージらしいです。



#### Step.1 Nodejs express appsの構築
次に、nodejs expressとAzure AD 認証の例を探したところ、[Build Node.js Express apps with Microsoft Graph](https://docs.microsoft.com/en-us/graph/tutorials/node?WT.mc_id=Portal-Microsoft_AAD_RegisteredApps)というチュートリアルがありました。このチュートリアルはMicrosoft Graph APIからカレンダーの情報を得るというものですが、この中でAzure AD 認証のステップを踏んでおり使えそうです。グラフからはユーザー名くらいを取得するようにします。

チュートリアルに従って、nodejs expressのサーバーを立てていきます。チュートリアルのGitHubレポジトリのなかの、demoのなかにチュートリアルのコードが入ってたので、これをダウンロードしてきて作業しました。 https://github.com/microsoftgraph/msgraph-training-nodeexpressapp/tree/main/demo/graph-tutorial

この部分を詳しく書くと、ベクトルタイルではなくてMSALの話になるので、あまり深くはいりませんが、注意した点は以下の通りです。

（**このstepのここから下は半分以上は自分のメモみたいなので、不要なら読み飛ばしてください。**）

* msal application objectは、app.locals.msalClientに格納されている。
* サーバーではメモリストレージを使ってログインしたユーザーを保存している。デモなので、メモリ（app.locals）を使っている点には注意。本番環境では要検討。（これはPM2でクラスターモードで２以上にしても問題になる原因です。）
* Step1と違って、認証関係のコードはroutes/auth.jsに書かれている。authのcallbackプロセスでは以下のようなことをやっていました。
  * トークンリクエストで帰ってきたresponseのresponse.account.homeAccountIdをreq.session.userIdとセッションに記録している。さらにgraphからかえってきたユーザーの詳細情報を情報を、ユーザーストレージに入れている（req.app.locals.users[req.session.userId]）。そしてルートに（/）リダイレクトする。

* そんな感じで、ユーザー情報をセッションに保存しているようでしたが、これを使って、どのようにアクセス管理をしているかは、calendar.jsをみてみました。ここにリクエストがくると、以下のような感じでした。
    * req.session.userIdがないと、ログインしなさいということで、ルートにリダイレクトされる。
    * req.session.userIdがあれば、変数userとしてreq.app.locals.users[req.session.userId]がとってこられる。そして、このuserやmsal application objectをつかってとってきたgraphの情報が返される。

ここまではベクトルタイルと全く関係ないですが、こんな感じでユーザーの認証をしているようです。
このマイクロソフトのチュートリアルは2021年4月に1.8版に更新されたのですが、その前のバージョンを参考にして作った開発環境用のベクトルタイルサーバーが https://github.com/un-vector-tile-toolkit/coesite になります。[unvt/onyx](https://github.com/un-vector-tile-toolkit/onyx)より制限の多い環境でも頑張ってほしいという意味を込めてcoesiteという名前にしました。（onyxの由来を知らないのですがSiO2の組成の物質を意味しているなら、超高圧ではコース石になりますね。）

#### Step.2 ベクトルタイルへのルートを追加
これまでで、ユーザー認証してログインするサーバーの形ができたので、つぎはベクトルタイルへのルーティングを設定します。

ベクトルタイルを配信するためのルーティングをrouteの中につくります。イメージとすると、こんなルーティング（ https://github.com/ubukawa/server-test-01/blob/main/routes/VT.js ）に step 2 のユーザー確認をつけるようなイメージです。ちょっと不完全ですが、試してみたのが ここ( https://github.com/un-vector-tile-toolkit/coesite/blob/main/routes/vtile-m.js )にあります。そして、app.jsに**var vtileMRouter = require('./routes/(作ったファイルの名前)')** と、**app.use('/（パス）', vtileMRouter)** を加えます。
app.jsでCORSの設定も忘れずにします。

#### Step.3 httpsにする （そしてPM2で実行する）
マイクロソフトのチュートリアルはローカルホストのhttpでのホスティングなので、app.jsにspdyモジュールをつけて以下の感じでhttpsにします（キーのパスやポートは適宜定義しておく）。これでサーバーがhttpsで動きます。バックグラウンドで動かすためにはPM2を使って実行します。

```
//for https
spdy.createServer({
    key: fs.readFileSync(privkeyPath),
   cert: fs.readFileSync(fullchainPath)
}, app).listen(port)
```





#### その他：PM2の小ネタ
##### configフォルダのなかにdefault-0.hjsonという空ファイルをつくる
configパッケージでcongif/default.hjsonのオブジェクトを読み込めるようにしておくのですが、PM2を実行したときに、エラーが発生しました。default-0.hjsonという空ファイルを作っておいておいたら動きました。

##### クラスターモード
pm2の実行時に、iオプションでクラスターの数を指定できます。しかし、ユーザー情報をセッションメモリに保存していたので、別のクラスターからはメモリが参照できずに再度ログインが必要になってしまいました。メモリのところを直すまではクラスターモードは１でやりましょう。


## 課題
ここまでで、一応ログインしてベクトルタイル地図をみることのできるサーバーが作れました。しかし、まだ課題があって、現在作業しています。

#### クロスオリジンの認証
現在、クロスオリジンの対応でまだ調整を進めています。
認証をつけなければ、 **app.use(cors())** だけで他のオリジンからもベクトルタイルを使うことができるのですが、クロスオリジンで認証があるともう少し対応が必要になりそうです。

例えば、リクエストする側は、CORSでcredentialをincludeにしないといけなさそうですし、レスポンスの側はAccess-Control-Allow-Credentialsをtrueにしないといけないようです（例えば　このページの説明　https://web.dev/cross-origin-resource-sharing/　）

なので、サーバーでは **app.use(cors())** ではなくて、

```
const corsOption = {
 origin: '*',
 credentials: true
}
app.use(cors(corsOption))
```
としないといけないかもしれません。

また、地図ライブラリの方も、transformRequestということで、credentials: 'include'にしないといけないと思います。[mapboxのrequest parameters](https://docs.mapbox.com/mapbox-gl-js/api/properties/#requestparameters)でも[MapLibreのrequest parameters](https://maplibre.org/maplibre-gl-js-docs/api/properties/#requestparameters)でも同じようにやるようです。

しかし、これらのことをやってもまだうまくいかないので、いろいろ実験中です。

#### セッションストア
今、セッションメモリにユーザー情報を入れているので、本番環境にむけてはセッションストアを準備しようと思っています。


## まとめ
ベクトルタイルサーバーに認証をつけてみた経験を記録しました。

このあたりは、ベクトルタイルサーバーというか、ウェブサーバーに認証をどうつけるかという話なので、あまりベクトルタイルは関係ないかもしれません。その道のプロがやると簡単にできるのでしょうが、初心者が挑戦してみた記録として、暖かい目で見ていただけると幸いです。

私はnodejsでやりましたが、配信データが小さいならapacheとかnginxとかよく使われているウェブサーバーでやってしまうと、参考資料が多くてやりやすいかもしれません。

## 謝辞
一緒に取組んでいる同僚に感謝しています。（日本語は読めないでしょうが・・・）

## 参考資料等
本文中にリンクを貼っています