Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9042

初見3分(本当は12分)でAuth0を使ったOIDCログインを実装するまで

$
0
0
完全に初心者の趣味です。勤務先や所属する団体の活動とは関係ありません。 tl;dr OpenID Connect(OIDC) でログインする、Single Page App(SPA) もどきをNode.jsで作りました。 作ったSPAもどきで、Auth0を利用してみました。サインアップから、Auth0を使ったログイン完了まで、初見で12分で終わりました。 一応利用規約も簡単に目を通した。 一番迷ったのは無料サインアップまでのルート。 作ったSPAもどきの解説は後半です。 Google IDでログインするための設定も最後に。 まずは実際に動かしてみる Node.jsがlocalhostで動く環境が前提です。(localhostじゃないときはlocalhostって書いてある部分を適当に変えてください) Auth0のサインアップ Google検索で「Auth0」と入れて、広告の下のリンクをクリックします。 広告のリンクをクリックすると、サインアップページにたどり着きません。 右上の「無料トライアル」をクリックして、GitHubでも、Googleでも、好きなアカウントでサインアップします。 そういえば自分の名前すら入力していない気がする。 サインアップが終わったら、すぐダッシュボードが表示されます。 右上の、「+Create Application」をクリックします。 適当にアプリ名を考えてから、今回は「Regular Web Applications」を選択します。 多分、ここは何を選んでも大丈夫な気がします。 次の画面で、「Quick Start」が出ますが、そこは使わず、その隣の「Settings」タブを開き、 「Client ID」、「Client Secret」を確認します。(後でコピペします。) 下にスクロールしていって、「Application URIs」の中の、「Allowed Callback URLs」に、 http://localhost:3000/ と入力します。 さらに下にスクロールしていって、「Advanced Settings」を開きます。 一番右の「Endpoints」をクリックします。 この中の、「OAuth Authorization URL」と、「OAuth Token URL」を、後でコピペします。 Node.js コードのダウンロードと設定 下記のレポジトリをチェックアウトします。面倒なら、ここからソースのZIPを直接ダウンロードします。 ファイルを解凍したら、ターミナル、もしくはコマンドプロンプトでnpm install を実行します。 処理を待っている間に、app.jsをエディタで開き、3行目から6行目に、先ほどAuth0の設定ページで見つけた、 「OAuth Authorization URL」、「OAuth Token URL」、「Client ID」、「Client Secret」を、それぞれコピペします。 SPAもどきでは、node.js側でセッションを管理しているので、 最初に1回だけnode sqlite_init.js を実行して、データベースを作ります。 node app.js を実行します。 実際にログインしてみる ブラウザで、 http://localhost:3000 にアクセスします。 上から順に、(1)、(2)のボタンを押していくと、Auth0のログインページに行きます。 「Sign up」から、eメールとパスワードを入力してログインすることもできますし、デフォルトでGoogleログインが使えるように設定されています。 「Accept」を押すと、元のページに戻ってきます。 (3)を押すと、Node.js経由で、Auth0のサーバから、ユーザ情報(いわゆるIDトークン)を取得して、その中身をデコードして表示します。 ここまで実測12分で終わってしまいました。。始めたときは、タイムアタックするつもりなんてなかったので、終わってから時計を見てびっくりしました。最初のサインアップページに迷ったり、利用規約を読んだりしなければ、本当に3分で終わったんじゃないかと思います。 SPAもどきの作り方 作った理由 いわゆる普通のWebサイトで、GoogleやFacebook,Twitterなどを使ったソーシャルログインを実現する例はよく見るのですが、ネイティブアプリでのソーシャルログインは、各サービスがSDKを提供しているため、仕組みがよくわからず、ブラックボックスになってしまっている気がしました。 なので、ネイティブアプリを作るのはちょっと難易度高いですが、静的なHTML+JavaScriptで、サーバとはREST APIで通信して、ネイティブアプリっぽく振る舞う、SPAもどきを作ることで、仕組みを理解しようと思いました。 参考にしたサイト 日本におけるOpenID Connectの重鎮のお二方のブログを参考にアーキテクチャを考えました。いつもお世話になっております。 アーキテクチャ REST APIっぽく作りたかったので、簡単にできるNode.js + Express構成を使います。 SPA部分は、JavaScriptも含めて1つのHTMLファイルにしています。 (作り始めたときは、一部動的な部分があるかなと、ejsを使いましたが、結果的に完全に静的なページになっています。) OpenID ConnectでSPAを作る場合、インプリシットフローやハイブリッドフローで、SPAが直接IDトークンを取得するパターンがよく紹介されている訳ですが、IDトークンにもそれなりに個人情報が含まれる場合があることと、実装をシンプルにするために、サーバ、SPA間はセッションIDで管理することにしました。 ログインのフローはこんな感じです。(1)から(3)は、それぞれ実際のSPAもどきを使うときに押すボタンに対応しています。 通常Webサイトであれば、ユーザのログイン状態はセッションIDをCookieに保存することで管理します。 ですが、ネイティブアプリを想定して、セッションIDはAuthorizationヘッダーに入れることにしました。ブラウザ内ではlocalStorageに保存します。 こういう設計で本当に問題ないのかは、識者のアドバイスを頂戴したいところです。私は初心者なので。 各処理の解説 (1) ログインリクエスト SPAは静的なHTMLで、サイトを開いた時点ではセッションIDは付与していません。 Ajaxで、サーバに対して、セッションIDの払いだしと、IDP(ID Provider:認証サーバ、ここではAuth0)にログインするためのAuthorizationエンドポイントのURLや、パラメータを取得します。 ※ ここでAuthorizationエンドポイントのURLを取得しているので、例えばIDPをAuth0からGoogleに切り替える場合も、サーバ側の設定を切り替えるだけで、SPA(HTML/JavaScript)側の修正は不要になります。 ついでに、SPA側でも、CSRFから防御するため、stateという乱数を生成しておきます。 サーバ側では、ここでセッションIDを払い出し、ログインのためのPKCEコードを生成して、各種パラメータとともにSPAに返却します。 PKCEについての説明はこちら サーバから返ってきたセッションIDは、後で使うのでlocalStorageに保存しておきます。 今回作ったSPAでは、デモ用にセッションIDを表示していますが、通常は隠しておくべきです。 (2) ログインページに接続 (1)でサーバから返ってきた AuthorizationエンドポイントのURLとパラメータに、SPAで生成したStateをくっつけて、アクセスします。 ネイティブアプリであれば、外部ブラウザやCustomTab, SFSafariViewControllerやASWebAuthenticationSessionを使いますが、今回はブラウザなので単純にlocation.hrefで移動することにします。 遷移先で無事ログインが成功すると、元のページに、クエリパラメータ「code」と「state」が付属して返ってきます。 ネイティブアプリであれば、Android App LinksやUniversalLinksを使ってアプリに処理を戻すことが推奨されます。詳細はBCP212/RFC8252を参照してください。 今回作ったSPAでは、エラー処理は省略しています。 (3) 認可コードとセッションID送信 SPAに認可コード(code )とstateが返ってきたので、まず、stateが事前に保存しておいた値と一致するかを検証します。もし一致しなかったら、悪意ある人からの攻撃の可能性があります。 ※ といっても、PKCEを使っている限り、その先の処理が成功しないのでユーザ情報の流出などの可能性は低いですが、stateを使わずに、codeが戻ってきたら自動でサーバに送信してログインを試みるような処理をしていると、勝手にログアウトされちゃったりしかねないので、検証処理を入れておいた方がいいと思います。 state が一致しているようなら、サーバに、認可コードを送信します。その際、(1)で保存しておいたセッションIDを、Authorizationヘッダーに付与します。 今回作ったSPAでは、デモのために、(3)の処理を手動で行うようにしていますが、本来であれば、認可コードがIDPから戻ってきてから、Stateを比較し、サーバに送る一連の処理は自動で行うべきです。 認可コードを受信したサーバは、セッションIDと紐付けて保存しておいたPKCEコードと、クライアントID/クライアントシークレット、認可コードをIDP(Auth0)に対して送信します。 すると、IDPから、IDトークンが返ってきます。 IDトークンをデコードすると、ユーザID(sub値)がわかるので、サーバの中では、sub値をキーにしてユーザを管理します。 サーバ内で、セッションIDと、sub値を紐付けてデータベースに保存したら、ログイン完了です。 SPAでは、デモのため、IDトークンの中身を返却しています。 (4) セッションIDの利用 今までの処理で、セッションIDとユーザが紐付いたので、セッションIDが安全に管理されている限り、セッションIDの送り主=そのユーザであるということを前提にサービスを提供します。 今回作ったSPAでは、セッションIDを送ることで、ログイン時にサーバに保存したユーザ情報を取得することができるようにしてみました。 試しにブラウザをリロードして、(4)のボタンを押してみてください。(3)を実行したときと同じ情報が返ってくることが確認できます。 Google IDでログインしてみる。 今度は、Auth0を使わずに、直接Google IDでログインできるようにしてみます。 Google ログインの設定 Google IDでOIDCログインを行うための設定は、Auth0に比べると、少し大変です。 まず、Google Cloud Platformで新しいプロジェクトを作ります。 そして、「認証情報」を開き、「+認証情報を作成」から、「OAuthクライアントID」を選択します。 すると、先に同意画面を設定しろと言われてしまいました。言われたとおり、「同意画面を設定」をクリックします。 User Typeは、企業ユーザなどでない限り、「外部」しか選択できません。 アプリ名など、必要な情報を適宜入力します。 スコープを選択するよう求められますので、最低限`openidだけは指定します。 次の画面で、テストユーザを登録するよう求められますが、登録しなくてもログインだけはできるようです。 仕切り直して、「認証情報」を開き、「+認証情報を作成」から、「OAuthクライアントID」を選択します。 アプリケーションの種類は「ウェブ アプリケーション」としました。 承認済みのリダイレクトURIに、http://localhost:3000 を追加します。 「作成」を押すと、「クライアントID」と「クライアント シークレット」が表示されます。 これを、先ほどダウンロードしてきた、app.jsにコピペします。 ドキュメントを見ても、Googleの「OAuth Authorization URL」、「OAuth Token URL」の設定はどこなのか、サンプル電文以外で見つけることができませんでした。おそらく、OIDC仕様に準拠して、OpenID Configurationを見ろということなんだと思います。(GitHubのソースコードは、元からGoogleの設定にしてありますので、Auth0をまだ試してみていない場合は、そのままで大丈夫です。) app.js const OIDC_AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth" const OIDC_TOKEN_ENDPOINT = "https://www.googleapis.com/oauth2/v4/token" Google IDでログインしてみる。 設定が終わったら、node app.jsをCtrl+Cで終了してから再起動して、新しいタブを開いて 、http://localhost:3000に再度アクセスして見てください。 (1),(2),(3)と順番にボタンを押していけば、今度はGoogleのIDトークンの中身が取得できました。 ちなみに、「新しいタブを開いて」とお願いした理由は、冒頭で取得したAuth0のセッションが残っていれば、 そっち側のセッションもちゃんと有効で、Auth0で取得したユーザ情報を取得することができます。 GoogleとAuth0で、iss値が異なるのがわかると思います。 セッションDBの中身 今回作ったSPAはデモなので、1つのテーブルですべてのデータを管理しています。PKCEコードとか、ログイン終わったら消してもいいんですけどね。 あと、DBの中を直接見て気づいたんですが、Auth0のsub値には、|(縦棒) が含まれてますね。いまどきいないと思いますが、SQLを生でいじるような人は要注意ですね。 % sqlite3 oidc_db.sqlite ".schema" CREATE TABLE session (session_id TEXT UNIQUE NOT NULL, pkce_code TEXT, subject_id TEXT, user_info TEXT, active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (DATETIME('now', 'localtime'))); % sqlite3 oidc_db.sqlite "select * from session" C2WRrorCttmBOuY9g6FXbw|jp(中略)Gv|auth0|610e48bed3fd1200687bb379|{(中略)"iss":"https://oidc-for-spa.jp.auth0.com/",(中略)}|0|2021-08-07 18:38:58 6DvaFpPv5rVUioruwnBXxw|QU(中略)6L|113688351101139242200|{"iss":"https://accounts.google.com",(中略)}|1|2021-08-07 21:49:40 感想 OAuth / OpenID Connect という標準に準拠して各社がサービスを提供してくれているおかげで、最低限の設定変更で、各社のIDaaSを使って、Webやアプリでのログインが実現できることがわかりました。標準化バンザイ。

Viewing all articles
Browse latest Browse all 9042