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

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

タグ:nodejs

自分が作るアプリの中でユーザー認証/登録といった機能が必要な場合、自分の場合は自作することはほとんどなく、IDaaS 等と呼ばれている SaaS 系専用サービスを使って実装しています。具体的には(資料が充実しているという理由ですが) Auth0AppID を使うことが多いです。単なるユーザー認証だけでなく、(実装する場合に面倒な)オンラインサインアップやパスワード再設定、パスワード忘れ時のリセット機能などをサービスが既にセキュアな形で組み込まれていて、自分ではほぼ実装を意識することなく使えて便利※です。

※もう少しちゃんと説明すると、Auth0 の場合であれば認証自体は OAuth を使って auth0.com で行い、認証情報が正しければコールバックで受け取った情報を使ってアクセストークンを取得できる、というロジックを簡単に実装できます。


この Auth0 を使ってアプリを作った場合、ユーザー名(表示名)やそのアイコン画像は(特定のルールに基づいて)登録時に決まります。ユーザー名はともかく、特にアイコン画像は自動生成されたものではなく自分で気に入っているものを使いたいと感じることが多いと思っています。が、現状ユーザー本人が直接変更することはできません。登録時に決まったものを使い続ける、ということになるので、こだわりを持ったユーザー名やユーザーアイコンへの変更ができないことを問題と感じていました。

で、Auth0 を調べたところ「変更用の API 自体は提供されている」ことが分かりました。要はユーザーが直接変更する機能は提供されていないが、その機能を自分で自分のアプリ内に作る方法は提供されている、ということになります。これを Node.js で早速試してみたのが今回のブログエントリのテーマです。


【プロフィール画像変更 API】
例えばプロフィール画像を変更する API 自体はこのように提供されています(下図は Node.js のサンプル):
2023022001


この上図で言うところの USER_ID 部分に画像を変更したいユーザーの ID(Auth0 上での User.id)を指定し、また変更後の画像 URL を picture の値として指定した上で上述の REST API を実行すればよい、ということになります。

特に Node.js の場合は同様の処理をより簡単に実行できるような SDK も提供されており、こちらを使った場合は ManagementClient クラスの update メソッドに必要なパラメータを渡して実行するだけです。

なおどちらの例もユーザーのアイコン画像(picture)だけを変更する例として紹介されていますが、実際には picture 以外にも nickname(表示名)や firstName, lastName など他の多くの属性を変更することも可能です。 実装そのものは後者(SDK)の方が簡単なので、以下こちらを使って説明を続けます。


【client_id と client_secret の準備】
この API を実行するには Auth0 の認証アプリを作る時と同様に client_id と client_secret (と domain)情報が必要です。ただ多くの場合、取得し直しが必要になる点に注意が必要です。以下は Auth0.com にログインし、メニューから Applications - Applications を選択した後に "Create Application" を実行した時の画面です:
2023022004


Auth0 を使って自分のアプリに認証機能を付加する場合、そのアプリの種類によって "Native(スマホ向けネイティブアプリ)", "Single Page Web Application(SPA)", "Regular Web Applications(その他のいわゆる「ウェブアプリ」)" のいずれかから1つ選んで(Auth0 の)アプリケーションを登録することが多いはずです:
2023022002


ですが、今回の機能を実現するためには "M2M(Machine to Machine)Applications" の client_id と client_secret が必要になります。そのため、まだ M2M アプリを作ったことがない場合は Machine to Machine Applications を選択して、新たに1つ追加登録してください:
2023022003


次の画面で対象 API を選択しますが、M2M アプリの場合は "Auth Management API" の1つしか選択肢がないので、これを選択します:
2023022006


最後に API の許可スコープを指定します。今回はユーザー管理機能を実装するので、検索バーに "users" と入力し、そこで見つかる全てのスコープにチェックを入れて、最後に "Authorize" ボタンをクリックするとアプリケーション登録が完了します:
2023022007


こうして Auth0.com に作成(追加)した M2M アプリケーションの Client ID、Client Secret(と Domain ですが、Domain は他のアプリ登録時に取得したものと同じはずです)が必要になります。利用時にすぐ取得できるようコピー&ペースト等で準備しておいてください:
2023022005


【実装】
ここまでの準備ができたら実装してみましょう。Node.js の場合は auth0 という便利なクライアントライブラリの SDK があるので、これを使って実装します。なお最後にサンプルアプリ・ソースのリンクを載せておくので、興味ある方はそちらをダウンロード後に実際に動作確認してみてください。

まず auth0 をインスタンス化します:
//. Auth0 Management API
var ManagementClient = require( 'auth0' ).ManagementClient;
var auth0 = new ManagementClient({
  domain: 'xxxx.auth0.com',       // domain の値
  clientId: 'client_id',          // client_id の値
  clientSecret: 'client_secret',  // client_secret の値
  scope: 'create:users read:users update:users'
});


auth0 の ManagementClient を取得し、domain, client_id, client_secret の3つの値と、スコープとして 'create:users read:users update:users' を指定してインスタンス化します(今回はプロフィール画像 URL を更新するため、これら全てがスコープとして必要になります)。繰り返しになりますが、重要なのはここで指定する client_id や client_secret は通常のネイティブアプリケーション/ウェブアプリケーションのものではなく、M2M アプリケーションとして登録したアプリケーションのものを使う必要がある、という点です。

そして実際に user_id = 'abcabc' のユーザーのプロフィール画像 URL を変更する場合は、以下のような処理を行います:
var params = { user_id: 'abcabc' };  // ユーザーIDが 'abcabc' の人が対象
var metadata = { picture: 'https://manholemap.juge.me/imgs/logo.jpg' };  // プロフィール画像を指定 URL のものに変更する
auth0.users.update( params, metadata, function( err, user ){
  if( err ){
    console.log( { err } );
        :  // エラー時の処理
  }else{
    console.log( { user } );
        :  // 成功時の処理
  }
});


実行する関数は auth0.users.update() で、この実行時パラメータとして第一パラメータに対象ユーザーの情報、第二パラメータに変更内容を指定します。実行後にエラーか成功かを識別して処理を続けます。

上の例だとプロフィール画像だけを変更していますが、複数の属性を同時に変更することも可能です。例えば以下の例は対象ユーザーのプロフィール画像(picture)と表示名(nickname) を同時に変更しています:
var params = { user_id: 'abcabc' };
var metadata = { nickname: '俺', picture: 'https://manholemap.juge.me/imgs/logo.jpg' };
auth0.users.update( params, metadata, function( err, user ){
  if( err ){
    console.log( { err } );
        :
  }else{
    console.log( { user } );
        :
  }
});


参考になれば、と思って実際に動くサンプルアプリを用意しました:
https://github.com/dotnsf/auth0-userpicture


サンプルアプリを実際に動かす場合は Auth0 に(M2M ではない通常の Web)アプリケーションも登録し、その client_id, client_secret, domain を取得し、また OAuth のコールバックURL(callback_url) を実行時の環境変数に登録する必要があります。実行時の環境変数登録時は同リポジトリ内の .env ファイルにその内容を記載することでも代替可能です。



参照していただくとわかるのですが、実際にユーザーの属性を変更しようとすると対象ユーザーの ID が必要になります。ユーザー ID は OAuth でログイン後にわかるので、実際のアプリケーションでは普通の(通常のウェブの)アプリケーションと、ユーザー管理用の M2M アプリケーションの2つを登録する必要があり、それぞれの client_id と client_secret の両方を使って実装することになります。詳しくはソースコード内を参照ください。


マーメイド記法を勉強している中で生じたアイデアを実装したら、なんとなくノーコード/ローコードっぽい仕上がりになり、自分で使っていても便利に感じたので公開して紹介することにしました。


【マーメイド記法とは】
最初に「マーメイド記法」について紹介します。マーメイド記法とは「特定のルールでテキストを記述することでフローチャート図を作る」ための記述ルールです。マークダウン記法の一種ですが、実際にマークダウンの中に含める形で使うこともできるようなサービスもあります。有名どころでは今年のバレンタインデーに GitHub 内のマークダウンファイル(.md)内にマーメイド記法で記述された部分があると、フロー図に変換して表示されるようになりました。

例えばこんな内容を含むマークダウンファイルを GitHub にコミットします(1行目はマークダウンそのものですが、3行目の ``` (バッククォート3つ)から ``` までの部分がマーメイド記法で書かれています。後述しますが、もう少し情報量を増やした書き方もあります):
2202110601

(↑なんとなくわかりますかね? AからB、AからC、BからD、CからD、という4つの流れが定義されているものです。先頭行の "graph DB;" は「フロー図を上から下(TopDown) に描く」という意味です)

そしてコミットされたファイルをブラウザで GitHub から見ると、マーメイド記法で書かれた部分がこのようにフロー図に変換されて表示されます:
2202110602



このマーメイド記法はウェブアプリケーション設計時の「画面遷移の定義」の表現に使えると思いました。例えばこんな感じのウェブアプリケーションの画面遷移を考えた時に・・・
 ・最初にログイン画面
 ・ログイン後にトップ画面
 ・トップ画面からA、B、Cの3つの画面に遷移できる

マーメイド記法だとこのように表現できると思ったのです:
```mermaid
  graph TD;
  Login --> Top;
  Top --> A;
  Top --> B;
  Top --> C;
```

また上でも少し触れたのですが、このマーメイド記法はかなり単純な最小限の情報しか含んでいませんが、もう少し情報量を増やしてこのような書き方もできます:
```mermaid
  graph TD;
  Login["ログイン"] --"/"--> Top["トップ"];
  Top --"/info"--> A["商品情報"];
  Top --"/download"--> B["ダウンロード"];
  Top --"/about"--> C["○○について"];
```


このマーメイド記法をフロー化するとこのようになります:
2022110503


A や B といったノードの名前そのままではなく、["~~"] で括られた部分がノードの表示用の名称として表示されています(一度 Top["トップ"] と表示名を定義しておくことで、2回目からはただ Top と書くだけでよくなります)。加えて矢印にも名前が付けることで、なんとなく UI のないワイヤーフレームっぽくなりました。どのページからどのページへ、なんという URL パスで進むようになるか、というのが視覚化できます(上の図の「ログイン」ページに相当する URL パスがないのでまだ不完全ですが、詳しくは後述します)。


マーメイド記法はフロー図だけでなく、色々な種類のグラフを描くことができます。ただマーメイド記法そのものの書き方についてはこのブログエントリの目的ではないので、他のサイトでの紹介を参照いただきたいです。私自身はこのページに多くお世話になりました:
https://zenn.dev/okazuki/articles/learning-mermaid


【アイデアと、実装して作ったツール】
このマーメイド記法を使って作ったファイルを使って、以下のような3つの機能を作れないだろうか、というアイデアを思いつきました:
 (1)ワイヤーフレームの全画面を表示できるようなウェブアプリ化
 (2)ワイヤーフレームの中で DB 定義が必要と思われる部分を抜き出して API 化
 (3)(おまけ)マーメイド記法で書かれた部分をフロー図化

(1)は上で示したようなフローを実現できるような最小限のウェブアプリを、マーメイド記述されたファイルから、各ページの URL パスをそのまま再現する形で自動生成する、というものです。 各ページの UI は最小限ですが、例えば上の例であれば「ログイン」ページを表示すると「トップ」ページに遷移できるようなリンクがあり、「トップ」ページを表示すると「商品情報」、「ダウンロード」、「○○について」という3つのリンクがあって、それぞれのページに繋がっている、というものです。フロー定義から画面実装までを自動化して、生成された HTML テンプレートをカスタマイズすることで見た目も定義できるようにする、というものです。

(1)についてはもう1つ、(2)にも関わるのですが例えばマーメイド記述された中に "/(複数形)""/(単数形)/:id" というフォーマットに合致する2つのパスが含まれていた場合は、前者を一覧ページ、後者を詳細ページとみなして、それに近い UI を用意する、という機能を持たせました。例えば、マーメイド記述が
```mermaid
  graph TD;
  Login["ログイン"] --"/"--> Top["トップ"];
  Top --"/users"--> A["ユーザー一覧"];
  A --"/user/:id"--> B["ユーザー"];
    :
```

のようになっていた場合、単に「ユーザー一覧」画面と「ユーザー」画面がリンクでつながっているのではなく、「「ユーザー一覧」は「複数のユーザーの一覧画面」」で「「ユーザー」画面は「ユーザー ID が :id となる単数のユーザーの詳細画面」」であることを識別できるようにしました。自動生成される画面も単なるリンクで繋げるのではなく、「ユーザー一覧」画面にはユーザーが複数表示され、それぞれのユーザーがクリックできるようになっていて、クリックするとクリックしたユーザーの詳細ページに遷移する、というものです。

そして(2)の機能は、この「ユーザー一覧」画面と「ユーザー詳細」画面があるということは「ユーザーのデータを扱う必要がある」と判断し、ユーザー情報を CRUD できるような API (と Swagger ドキュメントと DDL)を自動生成する機能です。具体的には、
 ・POST /user (一人のユーザーを新規作成する)
 ・POST /users (複数のユーザーをバルクインサートする)
 ・GET /user/:id (一人のユーザーの情報を ID を指定して取得する)
 ・GET /users (複数のユーザーの情報をまとめて取得する)
 ・PUT /user/:id (ID を指定したユーザーの情報を更新する)
 ・DELETE /user/:id (ID を指定したユーザーを削除する)
 ・DELETE /users (複数のユーザーをまとめて削除する)
という7つの CRUD API を作る、というものです。加えてこれらの API のデータ格納先となるテーブルの DDL 定義(id と name の2列だけですが・・)と、実際に動作確認できる Swagger API ドキュメントも作成します。


最後におまけの(3)、これはマーメイド記法で表記した内容を GitHub にコミットしなくても見ることができるよう、ローカルでマーメイドファイルを表示できるようにするツールもあると便利だったので、自分用に作ってみました。

こうしてできあがったのがこのツールで、現在は Node.js 向けソースコードを GitHub で公開しています:
https://github.com/dotnsf/web-template


【ツールの使い方】
このツールを使う場合は、まずは Node.js の環境を用意する必要があります。必要に応じてインストールしてください:
https://nodejs.org/ja/


次に git でソースコードを clone して、"npm install" で動作に必要なライブラリをインストールします:
$ git clone https://github.com/dotnsf/web-template
$ cd web-template
$ npm install

そしてアプリケーションや API サーバーのソースコードを生成するためのマーメイドファイルを用意します。ソースコード内に mermaid_sample.md というファイルが含まれていますので、これをそのまま使っても構いませんし、自分で用意しても構いません。

ただ自分でマークダウンファイルを用意する場合は次の点に注意してください:
  • 各ノード(ページ)の表示名を定義する場合は ["~~"] という形で記述する(マーメイドのルールでは ("~~") など、丸括弧で括っても正しく判断されますが、このツールを使う場合は ["~~"] という四角括弧で表示名を括ってください。
  • 全てのノート(ページ)を表示するための URL パスが定義できているようにマーメイドファイルを用意してください。

例えば上で紹介したこのマーメイドファイル例:
```mermaid
  graph TD;
  Login["ログイン"] --"/"--> Top["トップ"];
  Top --"/info"--> A["商品情報"];
  Top --"/download"--> B["ダウンロード"];
  Top --"/about"--> C["○○について"];
```

は一番上にある Login["ログイン"] ページを表示するための URL がどこにも定義されていません(ので、このまま実行してもエラーになります)。マーメイドファイルのどこかに(例えば以下の例のように)このログインページへの URL パスを追加する必要があります(mermaid_sample.md ファイルは初めからこのようになっています):
```mermaid
  graph TD;
  Login["ログイン"] --"/"--> Top["トップ"];
  Top --"/info"--> A["商品情報"];
  Top --"/download"--> B["ダウンロード"];
  Top --"/about"--> C["○○について"];
  Top --"/login"--> Login;
```

最後にトップページからログインページに遷移するためのパス /login を定義する行を加えました。これで全てのノード(ページ)の URL パスが定義されたことになります。もし実際のアプリケーションではトップページからログインページへのリンクが不要であれば、生成されたトップページからログインページへのリンクボタンを手動で削除してください(具体的には以下で説明します)。

なお最初の状態のマーメイドサンプルファイル(mermaid_sample.md)の内容は以下のようになっています:
# mermaid

```mermaid
  graph TD;
      Login["ログイン"] --"/"--> Top["トップ"];
      Top --"/items"--> Items["アイテム一覧"];
      Items --"/item/:id"--> Item["アイテム詳細"];
      Top --"/users"--> Users["ユーザー一覧"];
      Users --"/user/:id"--> User["ユーザー詳細"];
      Top --"/about"--> About["サイト解説"];
      Top --"/login"--> Login;
```

「アイテム一覧(/items)」と「アイテム詳細(/item/:id)」、「ユーザー一覧(/users)」と「ユーザー詳細(/user/:id)」という組み合わせが定義されていることを覚えておいてください。

また、このフローをマーメイド対応のビューワで表示すると、このようになります:
2022110504


マーメイドファイルが用意できたら実際に動かしてみましょう。まず(1)のワイヤーフレームの全画面を表示できるようなウェブアプリを作ってみます。この場合はソースコード内の web.js を使って、以下のように実行します:
$ MERMAID=mermaid_sample.md node web

実行時に MERMAID という環境変数を指定しています。この環境変数にマーメイドファイル名を指定します(上の例では mermaid_sample.md ファイルを指定しています)。

マーメイドファイルに問題もなく、正しく実行された場合は一瞬で実行が完了し、web/ というフォルダに実行結果が生成されています。実際に生成された結果を確認するには web/ フォルダで "npm start" を実行します:
$ cd web
$ npm install
$ npm start

成功すると 8080 番ポートでウェブアプリケーションが待ち受けているので実際にアクセスして動作を確認してみましょう。まずログインページにアクセスするには(マーメイドファイル内で)/login へアクセスすることでログインページが表示されるよう定義していたので、ウェブブラウザを起動して "http://localhost:8080/login" にアクセスしてみます:
2022110505


本来であれば「ログイン」ページには ID とパスワードを入力するフィールドがあって、、というものだと思いますが、残念ながらそこまでは再現できていません。ただこのページには「トップ」と書かれたボタンがあり、このボタンをクリックすることで(マーメイドファイルの中で定義されていたように)トップページへ遷移するようにできています:
2022110506


トップページは↑のようになります。マーメイドファイルではトップページから「アイテム一覧(/items)」、「ユーザー一覧(/users)」、「サイト解説(/about)」、「ログイン(/login)」という4つのページへのリンクが定義されていたので、それらがそのまま4つのボタンになっています。正しく再現できています。

試しに「アイテム一覧」ボタンをクリックすると以下のようになります:
2022110507


先ほどまでのページとは少し違う画面になりました。これはアイテム一覧のページ(/items) がアイテム詳細(/item/:id) のページと繋がっていて、これらの URL パスのルール性から「個別のページ」ではなく「item の一覧」のページであると判定された結果です。その結果、表形式で item の一覧(id と名前だけですけど)が表示されています。そしてこれらのいずれかをクリックすると(例えば id = 0 の一番上のデータをクリックすると)/item/0 へ遷移し、詳細画面が表示されます(トップ画面からユーザー一覧ボタンをクリックした場合も同様の画面遷移となります):
2022110508

※(注意)
この「一覧」と「詳細」の両ページの存在を確認するには、マーメイドファイル内の URL パスで
- /(複数形)
- /(単数形)/:id
という2つの URL パスが定義されていることが条件なのですが、この「複数形」の表現には注意が必要です。

"user" の複数形は "users" で、"item" の複数形は "items" で何も問題はないのですが、例えば "category" の複数形は注意が必要です。英単語として正しくは "categories" となりますが、本ツールでは単に "s" を付けるだけの "categorys" と定義する必要があります。"information" も(正しくは複数形が存在しない英単語ですが、もし information の一覧ページを作りたいのであれば)"/informations" という URL パスで一覧ページを定義する必要がある、という点に注意してください。
※(注意ここまで)


一方、トップ画面から「サイト解説」や「ログイン」ボタンをクリックした場合は、遷移先が一般的なページ画面であると認識され、表形式ではなく(リンクがある場合は)ボタンが表示される画面となります(下図は「サイト解説」ボタンを押した場合):
2022110509


・・・と、このように web.js はマーメイドファイルを指定するだけで、その内容からここまでのページ遷移を実現するウェブアプリケーションが自動生成できました。実際に詳細なワイヤーフレーム画面を作りたい場合は生成されたフォルダの views サブフォルダや public サブフォルダに HTML テンプレートや CSS, JavaScript ができているので、これらをカスタマイズすることでデフォルトの生成画面を(HTML や CSS の知識だけで)カスタマイズできる、というものです。


次に同じマーメイドファイルから(2)の API を作ってみます。(1)でも紹介しましたが、今回使用するマーメイドファイルには「ユーザー(user)」と「アイテム(item)」という2つのデータを対象に一覧表示したり、詳細表示するページが含まれていました。ということは user と item の2つはデータベースに格納して読み書き更新削除する可能性があるデータ、という推測ができることになります。

この推測通りに動くかどうかを実際に使って確認してみます。元の web-template フォルダで今度は api.js を以下のように実行します:
$ MERMAID=mermaid_sample.md node api

この実行結果は api/ フォルダに出力されます。今度も(1)と同様に api/ フォルダに生成されたコードを実行してみます:
$ cd api
$ npm install
$ npm start

成功すると 8081 番ポートで API アプリケーションが待ち受けており、また /_doc/ という URL パスで Swagger ドキュメントが稼働しています。実際に Swagger にアクセスして動作を確認してみましょう。"http://localhost:8081/_doc" にアクセスしてみます:
2022110510

2022110511


↑のように、item および user の情報を CRUD できる API と、その Swagger ドキュメントが用意されました。データはメモリに格納するので再起動するまでの間であれば実際にデータを保存したり、変更したり、削除したり、読み出したりすることができます。マーメイドファイルからデータ操作が必要な要素を見つけ出して API 化する(ためのソースコードを出力する)、という機能が実現できていることがわかります。 プロトタイピング目的であればこれだけでもかなり使えるところまで設計を可視化することができると思っていますが、(1)で紹介した web.js でマーメイドファイルからページ遷移機能と、(2)で紹介した api.js で API サーバーを両方作った上で、一覧ページと詳細ページの部分だけ API 連携するようカスタマイズすれば、細かな見た目以外のかなりの部分を実現できるプロトタイピングがほぼノーコード/ローコードで実現できると感じています。


最後におまけの(3)について紹介します。(3)はカスタマイズしたマーメイドファイルが、想定していた通りの正しいフローになっているかどうかを確認するためのマーメイド・プレビューワーとしてのアプリケーションです。(1)・(2)同様に環境変数 MERMAID にマーメイドファイルを指定して、以下のように実行します:
$ MERMAID=mermaid_sample.md node preview

成功すると 8000 番ポートでプレビューワーのアプリケーションが待ち受けています。実際にアクセスして動作を確認してみましょう。"http://localhost:8000" にアクセスしてみます:
2022110512


マーメイドファイルのプレビューができました。これで正しい(自分の想定通りの)ワイヤーフレームがマーメイドファイルとして実現できているかどうかを(GitHub などにコミットしなくても)事前に確認することができます。おまけツールではありますが、(1)や(2)を実行する前に何度か使うことになる利用頻度の高いツールであるとも思っています。


【まとめ】
もともとはウェブアプリのデザインとワイヤーフレームを効率よく作るためのプロトタイピングツールを作ろうとしていたのですが、マーメイドファイルとの相性がよく、データ API まで作れるようになったことで、ノーコード/ローコードツールともいえるかな、という形になりました。実際にはここで紹介していないパラメータを使うことで CRUD API を本物のデータベースと接続して作ったり、UI 部分も(デフォルトでは Bootstrap ベースで作るのですが)Carbon や Material デザインにしたり、ということもできるようには作っています。 興味あるかたは公開されているソースコードや README を見て試してみてください。

Auth0 のプログラミング中にちとハマった内容を備忘録メモとして残しておきます。

【背景】
ウェブのサービスやアプリを作る際に、いわゆる IDaaS(ID as a Service : ユーザー管理機能の SaaS) を使うことが多くあります。自分がよく使っているのは Auth0AppID です。ユーザーのログイン/ログアウトだけでなく、作ろうとすると面倒なオンラインサインアップの機能などもまとめて API で提供されていて、ユーザー管理周りを任せてしまうことが多いです。特に Auth0 を Node.js で使う場合は専用のライブラリも提供されていて、これを使うことで簡単に連携サービスを作っていました。

先日、Auth0 を使ってサービスの開発をしている時に「ログアウト」がうまくできないことに気付きました。以前は以下のようなコードでログアウトできていたのですが、これだとエラーは発生しないものの、セッションなのかクッキーなのか、よくわかりませんが情報が残ってしまってログアウトできていませんでした(以下のコードだとログイン情報が残ったままトップページに遷移してしまう):
app.get( '/auth0/logout', function( req, res ){
  req.logout( function(){   //. ログアウトして、
    res.redirect( '/' );    //. ログアウト後にトップページ('/')に移動する
  });
});

あれ~、以前はこのコードで動いてたんだけどな・・・ と思ってよく調べてみると、2021 年12月にログアウト処理の仕様が変わっていたようでした:
Logout Redirects Migration Guide


このあたりを踏まえて、現状の Auth0 ログアウト実装方法を調べたので、以下に記載します。

【ログアウト実装準備】
まず準備段階として、Auth0 でログアウトを行う場合に「ログアウト後に移動する先の URL」をあらかじめ登録しておく必要があります。Auth0 ログイン後の Applications メニューから該当アプリを選択した先の "Allowed Logout URLs" という項目です:
2022090401


コールバック URL 同様に、ここにログアウト後に遷移する URL を事前に登録しておかないといけないようでした。自分の場合は開発時は localhost を使うので、これと本番時の URL (https://xxxxx.xxxxxxx.com)を合わせて(カンマ区切りで)以下のように指定しました:
http://localhost:8080,https://xxxxx.xxxxxxx.com

これでログアウト処理を実装するために必要な準備は完了です。


【ログアウト実装】
現在のログアウト仕様は以下で確認できました。結論としてはクライアント(ブラウザ)が https://(Auth0 ドメイン)/v2/logout に必要なパラメータを付与して GET リクエストを出すことで正しくログアウトできるようです:
https://auth0.com/docs/api/authentication#logout

サーバーサイドのプログラム上だと以下のようになります:
app.get( '/auth0/logout', function( req, res ){
  req.logout( function(){   //. ログアウトして、
    //res.redirect( '/' );    //. ログアウト後にトップページ('/')に移動する
    res.redirect( 'https://(Auth0 のドメイン)/v2/logout?client_id=(Auth0 クライアントID)&returnTo=http://localhost:8080' ); 
  });
});

このうち、Auth0 ドメインと Auth0 クライアント ID はアプリケーションを登録する際に同時に確認/取得できるものです。また returnTo パラメータを付けないと、ログアウト処理は行われるのですが、そのまま処理が終わってしまうので、事実上 returnTo パラメータも必須です。このパラメータにログアウト後に遷移する URL を指定します。この URL は上述の準備時に Allowed Logout URLs で指定した中に含まれている必要があります(事前に指定した Allowed Logout URLs に含まれていないとエラー)。


この変更で正しくログアウトできるようになることが確認できました。ほっ・・・


とある要件を実現するツールを作りました。同じことに悩む人がいた場合を想定して、ツールをソースごと公開することにしました。

某ウェブプラットフォームのサービス終了が決まり(わかる人はこれだけで何の話か推測されそうだけど・・)、現在稼働中のウェブアプリケーションを引っ越しすることになりました。引っ越しそのものはさほど難しくないのですが、問題は「サービスの URL が変わってしまう」ことでした。

図に示すとこのような感じです。これまで運用していたサービスの運用環境を A から B に引っ越しした結果、これまでの URL とは異なる URL で引き続き運用することになりました:
2022070601


これまで使っていたユーザーに対しても「サービスの URL が変わった」ことを知らせてあげたいのですが、具体的にどうするべきでしょうか?A で運用中の画面に「URL が変更になった」と注意書きを含めて、改めて B にアクセスしてもらうこともできます。が、もう少し気の利くやり方として「A にアクセスしたら自動的に B に転送させて、B で運用中の画面で URL が変更になった旨を記載しておく(そのままブックマークできるようにする)」という方法もあります。


この後者の方法を実現するためには A にアクセスした利用者に対して "301" という HTTP ステータスコードと、続けて変更先の URL を Location ヘッダに含めて返すことで実現できます。この HTTP ステータスコード 301 は "Moved Permanentaly" を意味していて「URL が(一時的ではなく)恒久的に変更になった」ことを示しています。続けて Location ヘッダに新しい URL を含めておくことでウェブブラウザ側で新しい URL に自動的に遷移してくれます。つまり「A にアクセスしたら自動的に B に移動させる」ことが実現できます(そして B 側で「URL が変わったので現在のページをブックマークして」といったメッセージを記しておく、といった対応になります)。ウェブページの引っ越しを行う場合の一般的な手段でもあります:
2022070602



問題となるのは、この「301 という HTTP ステータスコードを返す」機能です。A 側のサービスでそのような転送機能や転送の設定が提供されていればそれを使えばいいのですが、必ずしも提供されていないことも考えられます。そのようなケースに対応するため、今回「アプリケーションの機能として 301 HTTP ステータスコードと、引っ越し先 URL を返すアプリケーション」を作ったので、公開することにしました。これまで A で動いてたアプリケーションの代わりにこのアプリケーション(下図の app)をデプロイすることで、A へのアクセスがあった場合に、新しい B への URL に無条件で転送させることができるようになります:
2022070603


このような挙動を実現するための Node.js アプリケーションのソースコードを以下で公開しました:
https://github.com/dotnsf/301movedpermanently

動作確認する場合は、Node.js が導入済みの環境にソースコードをダウンロードするか git clone して、環境変数 URL に転送先の URL を指定して実行します。なおアプリケーションはデフォルトで 8080 番ポートで待ち受けますが、このポート番号を変えたい場合は環境変数 PORT に指定して実行してください:
$ git clone https://github.com/dotnsf/301movedpermanently

$ cd 301movedpermanently

$ npm install

$ URL=https://www.yahoo.co.jp/ node app (全てのリクエストを Yahoo! トップページに転送する場合)

起動後にアプリケーションにアクセスすると、全てのリクエストが環境変数 URL で指定した値(上の例だと https://www.yahoo.co.jp/)に転送されます。なおコード内ではメソッド/パス/パラメータに関係なく、全てのリクエストを GET リクエストに変換して転送しています。GET/HEAD メソッド以外のリクエストについては HTTP ステータス 308 を返して対応するケースもありますが、今回のような「サービスごと引っ越し」のケースでは新 URL のトップページに転送することが多いと思うので、今回は説明を控えます(下の青字部分が実行されます):
app.all( '*', function( req, res ){
  if( post_redirect ){
    var method = req.method;
    if( method == 'GET' || method == 'HEAD' ){
      //. https://developer.mozilla.org/ja/docs/Web/HTTP/Status/301
      res.status( 301 );
    }else{
      //. https://developer.mozilla.org/ja/docs/Web/HTTP/Status/308
      res.status( 308 );
    }
  }else{
    res.status( 301 );
  }

  res.set( 'Location', url );
  res.end();
});

古いサイト A が動いている間だけ有効な強制転送方法ですが、他に方法がない場合はこんな感じで古いサイトを訪ねた人を強制的に B へ転送する方法が有効だと考えています。

なお、docker イメージとしても公開しているので、移行元で docker が使える環境であればこちらを使っていただくのが手っ取り早いと思っています:
dotnsf/301movedpermanently


REST API やフロントエンドフレームワークを使ったウェブアプリケーションを作っていると、CORS (Cross-Origin Resource Sharing) に悩まされることが珍しくありません:
cors.001


CORS とはウェブブラウザ(フロントエンド)の JavaScript におけるセキュリティ制約の1つで、AJAX などを使って HTTP リクエストを行う場合、原則的には同一オリジンからのリクエストだけが許可され、異なるオリジンからのリクエストは CORS 制約によって失敗するようになっています。バックエンドからの HTTP リクエストに関しては一切制約はありませんが、フロントエンドからのリクエストに対してのみ適用されるものです。詳しくはこちらもどうぞ:
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS


ただ REST API とフロントエンド・ウェブアプリケーションを作っている人が同一であったり、そういったリクエストが来るとわかっている前提で REST API を用意する場合など、API 提供側からもこの CORS 制約を無視したくなることがあります。そういった場合のために「REST API 側で特定のオリジンからの HTTP リクエストについては許可する」設定も可能です。具体的には許可するオリジン(https://allowed-origin.com)を指定して、
 Access-Control-Allow-Origin: https://allowed-origin.com
という HTTP レスポンスヘッダを返します。リクエスト元ではこのヘッダの内容を自分自身のオリジンと比較して、一致している場合のみ正常処理とみなす(一致していない場合はエラーとする)という処理が行われます。なお「全てのオリジンからのリクエストを許可する」という指定も可能で、その場合は例外的に
 Access-Control-Allow-Origin: *
という HTTP レスポンスヘッダを返すことで実現できるようになっています。

さて、実際にフロントエンド側を開発していると、以下のような問題に直面することがあります:
(1)開発中やテスト中は http://localhost:3000 で動かす
(2)開発終了後は https://xxxxx.com で実運用する
(3)REST API は(1)とも(2)とも異なる環境で稼働している(つまり CORS 設定しないと動かない)

開発中は自分の PC でコーディングするので http://localhost:3000 のようなローカルホストを使った動作確認となり、実運用中は運用のために用意した https://xxxxx.com というインターネット上のホストを使ってアクセスする、というケースです(特別に珍しいケースではないと思います)。開発(テスト)環境でも実運用環境でも REST API を動かす必要があるため、REST API 側ではいずれのケースでも動作するような CORS 設定ができると楽です(http://localhost:3000 からのリクエストを許可する是非はともかく)。しかし CORS の Access-Control-Allow-Origin ヘッダには1つのオリジン(それも * 以外の正規表現とかは使えずに)しか記述できない、という制約があります。つまり「http://localhost:3000 または https://xxxxx.com からのリクエストを許可する」という Access-Control-Allow-Origin の指定はできないのですが、これをうまく回避・実現する方法はないでしょうか?

1つの考えられる方法として「開発中だけは "Access-Control-Allow-Origin: *" を指定」して全てのオリジンからのリクエストを許可する、という方法も考えられます。が、これはあまりに無防備な設定でもあります。

そこで考えたのが以下の方法です。あらかじめ許可するオリジンを配列として用意した上で、リクエスト元が許可オリジン配列の中に含まれていた場合はそのオリジンを Access-Control-Allow-Origin ヘッダに動的に指定して返す、という方法です。Node.js + Express 環境向けに具体的に作るとこんな感じ:
// app.js
var express = require( 'express' ),
    app = express();

app.use( express.Router() );

var settings_cors = 'CORS' in process.env ? process.env.CORS : '';
app.all( '/*', function( req, res, next ){
  if( settings_cors ){
    var origin = req.headers.origin;
    if( origin ){
      var cors = settings_cors.split( " " ).join( "" ).split( "," );

      //. cors = [ "*" ] への対応が必要
      if( cors.indexOf( '*' ) > -1 ){
        res.setHeader( 'Access-Control-Allow-Origin', '*' );
        res.setHeader( 'Vary', 'Origin' );
      }else{
        if( cors.indexOf( origin ) > -1 ){
          res.setHeader( 'Access-Control-Allow-Origin', origin );
          res.setHeader( 'Vary', 'Origin' );
        }
      }
    }
  }
  next();
});

app.get( '/ping', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );

  res.write( JSON.stringify( { status: true, message: 'PONG' }, null, 2 ) );
  res.end();
});

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );


起動時の環境変数 CORS として許可オリジンの配列(以下の例では "http://localhost:3000" と "https://xxxxx.herokuapp.com" の2つ)をカンマ区切りで指定します:
(例)$ CORS=http://localhost:3000,https://xxxxx.herokuapp.com node app

このサンプル(8080 番ポートで起動)では GET /ping というリクエストに { status: true, message: 'PONG' } というレスポンスを返す REST API が定義されていますが、リクエスト元のオリジンが http://localhost:3000 か https://xxxxx.herokuapp.com のいずれかの場合は Access-Control-Allow-Origin ヘッダによって CORS の制約を回避して実行できます。

これでテスト環境、ステージング環境、本番環境などで CORS の設定を変えずに運用できる API サーバーが用意できそうです。


上記サンプルソースコードはこちらに用意しました:
https://github.com/dotnsf/multicors


このページのトップヘ