一から勉強させてください

最下級エンジニアが日々の学びをアウトプットしていくだけのブログです

Auth0のSilent Authentication (サイレント認証)とRefresh Token Rotation (リフレッシュトークンローテーション)を完全に理解した (い)

Auth0のSilent Authentication (サイレント認証)とRefresh Token Rotation (リフレッシュトークンローテーション)を完全に理解したい気持ちが急に高まってきたので書きます。 全体の流れとして

  • React SPA with Auth0での認可フローについて
  • Silent Authentication (サイレント認証)について
  • Refresh Token Rotationについて
  • まとめ

みたいな流れで書きつつ、Silent AuthenticationやRefresh Token Rotationは何を解決しようとしているのか、それぞれのリフレッシュ方法でどのような挙動になるのか、などについて理解を深めていきたいと思います。

また、React SPA with Auth0の認可フロー部分のイメージを沸かせるために以下のサンプルリポジトリを用意しています: https://github.com/danimal141/auth0-react-playground

こいつは

みたいな構成になっていて、アクセストークン取得まわりの動作検証に使ったものなのでよかったら見てみてください。ちなみにGraphQLとか使っちゃっていますが、今回の文脈においてGraphQLの知識は特に必要ありません。


React SPA with Auth0での認可フロー

Auth0のReact用SDK: auth0-reactを利用した場合、認可フローはこちらUnder the hood, it implements Universal Login and the Authorization Code Grant Flow with PKCE. と書かれているように、Authorization Code Flow with Proof Key for Code Exchange (PKCE)になる。より詳細な説明はこちらに書かれている。

PKCEは雑にいうと、「OAuth2のAuthorization Code Flowで認可コード奪われたらアクセストークンも奪われちゃうからツラいですよね、認可コード送信元も検証して、せめてアクセストークンは取られんようにしましょうや」っていう仕組み。こちらの記事の解説がめちゃめちゃわかりやすかったのでオススメ。

auth0-reactを使う場合、Authorization Code Flow with Proof Key for Code Exchange (PKCE)な実装はSDK側でサポートしてくれているので自前で code_challengeなどをハンドリングするような必要はない。

ちなみにauth0-reactは元々あったauth0-spa-jsをReactのHooksベースの実装でいい感じに抽象化しただけのものなので、実装詳細を知りたい場合はauth0-spa-jsの方のコードを読んだほうがよい (読まざるを得ない)。

ログイン -> アクセストークン取得 -> ログアウトまでのライフサイクル

会員登録済みでAuth0上にすでにユーザが存在する前提で、ログインからアクセストークン取得、ログアウトまでの流れはざっくり以下のようになると思う。

  • auth0-reactloginWithRedirectを利用してログイン処理を呼び出す
    • ログインボタンなどを設置して、そいつのClickイベントで loginWithRedirectを呼ぶみたいなイメージ
    • Universal Loginの画面が表示され、認証情報を入力する
      • ここでAuth0とのセッションが生成される
      • セッションの有効期限はAuth0のTenant SettingsにあるLog In Session Managementの項目で設定できる
  • Auth0のApplication (SPA)でcallback URLとして許可しているURLに?code=xxxのクエリパラメータ付きでリダイレクトされる
    • codeなどがURLに含まれててツラいが、auth0-reactonRedirectCallbackを指定しておくと勝手にその辺のツラいクエリパラメータが除外されたURLに勝手にリダイレクトしてくれる
  • 上記のcodeを使って {auth0_domain}/oauth/tokenにリクエストを投げてaccess_tokenを取得
    • アクセストークン以外にもscopeなど色々情報は返ってくるはずだけど、今回は割愛
    • auth0-reactgetAccessTokenSilentlyというメソッドがその辺やってくれている
  • トークンストレージはデフォルトではインメモリ
    • 画面をリロードしたらアクセストークンは再取得する必要がある
    • localStorageを利用することも可能 (後述するけど、リスクを理解せずノリで使うのは危険)
  • auth0-reactlogoutを呼ぶとログアウトし、Auth0ドメインとのセッションも切れる

Silent Authentication (サイレント認証)

上述したが、アクセストークンはデフォルトではインメモリにキャッシュされるので画面リロードなどで都度、再取得が必要になる。「Universal login -> ログイン成功 -> ?code=xxx付きでSPAにコールバック」の流れでは特に問題なかったが、それ以降、Auth0ドメインとのセッションが生きている状態でアクセストークンの再取得はどうするのか、またログインからやり直しなのか...?

Auth0はアクセストークンをリフレッシュする手段としてSilent Authentication (サイレント認証)という仕組みをサポートしている。2021年6月時点ではデフォルトでSilent Authenticationがデフォルトのアクセストークンリフレッシュ手段になっている。リフレッシュトークンは auth0-reactの実装でProviderにuseRefreshTokens=trueを渡すと利用できるようになる (その他、Auth0の設定側で offline_accessスコープをAPIで許可する設定なども必要)。

これは雑に説明すると、画面遷移などを介さずひっそりとセッションを確認してアクセストークンを再取得する仕組みで、Chromeのネットワークタブなどをよく見てみると{auth0_domain}/authorize?xx=xxへのリクエストで prompt=noneresponse_mode=web_messageが含まれているのが重要なポイントである。

prompt=noneをつけることで画面表示が行われない状態でセッション確認ができる。そしてresponse_mode=web_messageによってユーザからは見えない iframeを作って、「そいつから {auth0_domain}/authorizeにリクエスト -> codeの含まれたレスポンスを受け取る -> そいつをWeb Messaging API経由で親に渡してアクセストークン再取得」みたいな動きを実現している。

詳しくはこちらの記事がわかりやすく解説してくださっている。

こいつのメリットはlocalStorageを使わないで、かつ毎回ログインを要求されるみたいなクソな挙動を避けてアクセストークンを再取得できることだと思う。SPAはブラウザが前提になるのでネイティブと違ってセキュアなストレージがない。こちらの記事にあるようにlocalStorageは悪意のあるnpmライブラリ経由でシュッとトークンが抜かれるリスクがあるなど、セキュアではない。アクセストークンのようなデリケートな情報を長期間そこに入れておくのは危険なので、そういったリスクを避け、かつユーザ体験をそこまで損なわないこの仕組みは非常によい。

ただこいつも完璧ではなく、こちらに書かれているように、昨今のSafariのITPのような、3rd party cookieがブロックされがちなご時世ではSilent Authenticationがうまく動作しないケースがある。

実際、SafariChromeで3rd party Cookieをブロックする設定のままSilent Authenticationベースのアクセストークンの再取得を試みると期待通り動作せず、以下のような感じになる。

  • SPA (iframe) <-> Auth0ドメイン間でのリクエストのやり取りの際に3rd party cookieに当たる情報が取得できない
  • セッション確認失敗
  • login_requiredのError Responseが返るため、codeが取得できない
  • アクセストークンも取得できない

これを回避するにはAuth0のカスタムドメイン機能を使ってSPAをapp.example.com、ログインURLを login.example.comみたいに設定して回避するか、後述のRefresh Token Rotationがサポートされたリフレッシュトークンを使う、あたりが選択肢になってくる。

Refresh Token Rotation (リフレッシュトークンローテーション)

上述のようなSilent Authenticationの弱点をカバーするための手段として生まれたのがRefresh Token Rotation (という理解で合ってる?)。詳しい説明はこちらに書かれている。

これまでリフレッシュトークンに有効期限がなかったので、それをlocalStorageに入れるのはリスキーだったが、Refresh Token Rotationを使うとリフレッシュトークンを使ってアクセストークン取得する際に新しくリフレッシュトークンも再発行され、以前のものは無効化されるのでよりセキュアにリフレッシュトークンが使えるようになった。

もちろんリスクが無くなるわけではないが、有効期間をそれなりに短めに設定してlocalStorageにトークンをキャッシュするアプローチも悪くはないと思う。

Refresh Token Rotationを有効にしてlocalStorageをキャッシュに利用した場合のフローは以下のようになると思う。実際に色々設定をいじったり、 auth0-spa-jsのコードを読みながら挙動を確認したんですが、間違ってたらご指摘ください。

アクセストークンの有効期限が切れる -> リフレッシュトークンを使って再取得の流れ

まずアクセストークン取得からそいつの有効期限が切れる -> リフレッシュトークンを使って再取得するまでの流れは大体こんな感じかと思う。ちなみにアクセストークンの有効期限はデフォルトで1日、Auth0 APIのToken Expirationの設定に依存する。

  • 最初のログインでAuth0のセッションができる
    • ここでアクセストークンとリフレッシュトークンを取得
    • localStorageにこれらの情報が保存される
    • localStorageの有効期限はアクセストークンの有効期限と同じになる
  • アクセストークンの有効期限が切れた場合
    • localStorageのキャッシュもクリアされる
    • リフレッシュトークンを使ってアクセストークンを再取得
    • このタイミングでリフレッシュトークンも更新されるので、以前のリフレッシュトークンは無効になる
    • localStorage上の値も更新される

アクセストークンの有効期限が切れる -> リフレッシュトークンも有効期限切れ / 無効だった場合の再取得の流れ

次にアクセストークンが無効になって再取得しようにもリフレッシュトークンも期限切れ、もしくは無効だった場合は設定によって挙動が変わる。ちなみにリフレッシュトークンの有効期間はAuth0のSPAのRefresh Token ExpirationにあるAbsolute Lifetimeの設定に依存する。

  • リフレッシュトークンがinvalidとなって403が返る
    • セッション有効期間 > リフレッシュトークン有効期間な設定の場合
      • リフレッシュトークンが使えなかった場合のFallbackとして、Silent Authenticationが呼ばれる
        • セッション有効期間内の場合
          • 3rd party cookieが問題なく使える場合、ここでiframe経由でcodeを取得 -> アクセストークン、リフレッシュトークンも取得できる
          • 3rd party cookieが使えない場合、Silent Authenticationが失敗、内部的にlogout({ localOnly: true })が呼ばれる
            • ローカルのキャッシュやCookieauth0.is.authenticatedの値がクリアされる
            • ここではAuth0セッションは生きている想定なので振り出しに戻る (おそらく{auth0_domain}/authorizeresponse_mode=queryで再リクエスト -> code、アクセストークン、リフレッシュトークン再取得になるかな、あまり自信ない...)
        • セッション有効期間を過ぎている場合、再ログインを要求される
    • セッション有効期間 < リフレッシュトークン有効期間な設定の場合
      • リフレッシュトークンの有効期間が実質セッションの有効期間みたいになる

まとめ

  • Silent Authenticationはブラウザのあまりセキュアではないストレージに頼らないかつ、UXを損なわないアクセストークンのリフレッシュを実現できるが、3rd party Cookieの扱いに厳しくなってきた昨今、いつ動かなくなるかわからないというリスクがある
    • 代替案としてRefresh Token Roationのサポートされたリフレッシュトークンを利用する
    • もしくはAuth0のカスタムドメインを設定して、3rd party cookieを扱っているとみなされないよう設定することで解決することもできるかもしれない
  • Refresh Token Rotationはリフレッシュトークンをローテーションさせることで以前よりも気軽にトークン情報をブラウザのストレージに入れられるようになったし、3rd party cookie、いつ使えなくなるんだろうという恐怖から開放される(と信じたい)

参照

Auth0のRulesとHooksの違い、そしてActionsについて

最近、Auth0を試している。

Auth0自体は機能も豊富で品質も高く満足はしているのだが、「こういう機能を作る時、Auth0ではどうするんだっけ?」となることが多いのも事実。学習コストはそれなりにある印象。

今回はその中でもAuth0が提供するRules、Hooks、Actionsあたりがよくわからなくなったので雑にメモ。

Rules

ドキュメントはこちら

  • ユーザが認証した際に実行されるJavaScript関数
  • 関数は認証プロセスが完了するタイミングで実行され、Auth0のデフォルトの認証フローをカスタマイズ、拡張するために使用することができる
    • 例えば「認証時にEmail確認を必須にし、email_verified: trueでない場合には メールを確認してくださいメッセージを返すなど
    • 実装しなくても公式で用意されているRulesが結構ある

Hooks

ドキュメントはこちら

  • Auth0上の拡張可能な特定のポイントで動作するJavaScript関数
    • 要するにRulesと違って、ログインフロー以外のフローのカスタマイズにも対応している
    • 拡張できるポイントはこちら
    • 例えば「運営側でユーザを作成し、初期パスワード変更を必須にする -> パスワード変更完了時に初期パスワード変更完了フラグを追加する」など

Actions

ドキュメントはこちら

  • RulesとHooksを経て生まれた、俺が考えた最強のカスタマイズ的なやつ
  • 下書き保存やバージョン管理などにも対応している
  • アクションを定義 -> ログインフローやパスワード変更フローなど好きなフローの中に定義したアクションを組み込むことができる
    • フローが可視化されていて直感的にアクションをバインドできるのでよさそう
  • まだbeta版だが、おそらく将来的にRulesとHooksはこいつに統合されるのでは

まとめ

  • 現状、Rulesが一番情報が見つかるのでシュッとログインフローをカスタマイズしたいならRules, それ以外のフローならHooksとかがよさそう
  • Actionsはサポートされているフローすべて拡張可能だがまだbeta版
  • おそらく今後はActionsが主流になっていくと思われるのでActionsを早めに試し始めるのもアリ

Dockerとplantuml-serverを使ってPlantUMLの実行環境を用意する

手軽にPlantUMLをいじれる環境が欲しかったけど、軽く調べた結果、Macでの環境構築が悩ましかった。

色々検討した結果、ひとまずDockerで環境構築する方法をとった。

候補

  • ローカルでbrew install graphviz, brew install plantumlする
    • 変な依存ライブラリとかめっちゃ入れられそうなので、できればgraphvizをローカルに入れたくないw
  • PlantUML Viewerを使う
    • 悪くなさそうだったが、試しにいれてローカルのファイルを開いてみたがCORSっぽいエラーが出て面倒そうなのでやめた
    • ChromeウェブストアのレビューでもDoesn't work at all.って言ってる人いたので最近のChromeのアップデートで死んだのかな
  • plantuml-serverを使う
    • Dockerのimageがあるので一番手軽に使えそうだし、軽くPlantUMLを書いてUMLを確認できれば十分だったのでこれを使うことにした

環境構築

Vim

Vimを使っているのでPlantUMLを書く用に plantuml-syntaxだけ入れておいた。他のエディタを使っている人も何かしらシンタックス用のプラグインはあるはず。

Docker

上記のplantuml-server用のimageが公開されているのでシンプルにそれを使う。こんな感じ。

# docker-compose.yml

version: '3.7'

services:
  plantuml-server:
    image: plantuml/plantuml-server
    ports:
      - 8080:8080

あとは docker-compose upして open http://localhost:8080すればPlantUMLの実行環境が手に入り、サクッとUMLを確認できる。

まとめ

とりあえずVimでPlantUML書いて、ローカルでplantuml-serverを立ち上げてコピペでUML確認できるようになった。若干スマートさに欠けるけどまあいいかという感じ。

エンジニアの自分がインプットのためによく聴いているPodcastの番組まとめた

最近はなにがし.fmみたいな番組がめっちゃ増えてきましたね。

普段、移動時間やジョギングする時とかにPodcastをよく聴いているけど、何をどういう目的で聴いているか自分で整理するためにもジャンル別に聴いている番組をまとめてみました。

Tech系

Rebuild

  • 言わずもがなの番組、まだ番組が乱立してなかった頃からずっと聴いている
  • 話題になったTech系の話題や最新情報をキャッチアップできて良い
  • 最近はガジェットだったりコンテンツ系の話題が多い気がするけど、新しい言語とか新しい技術、マネジメント等について議論してる回は特に面白い
  • 英語回は英語の勉強も兼ねて死ぬほど繰り返し聴いた

fukabori.fm

EM.FM

  • これもマネジメントとかキャリア考える上で参考にしている

Misreading Chat

The Changelog

  • GoとかRustとかGraphQLとかイーサリアムとかトピックが個人的に刺さりまくることが多いのでチェックしてるけど、ガチの英語なので聴く頻度は少なめ
    • めちゃめちゃ集中しないと聴き取れないので元気な時だけ聴く
      • 集中しても全然聴き取れない
    • GoのRob Pikeがゲストで来てたりでアツい: https://changelog.com/podcast/3
  • 余談だけど、サイトはElixirでできてる: https://github.com/thechangelog/changelog.com

ビジネス系

前田ヒロ Startup Podcast

  • SaaS界隈のスタートアップに投資されている前田ヒロさんのPodcastSaaS系のゲストや話題多め
  • 個人的にも創業期のスタートアップにいた期間がキャリアの中で圧倒的に長いので、トピックが刺さりまくる
    • ゲストの起業家の方々のマインド、考え方、失敗談などをインプットしまくれるので高まる、よい
  • 2021年2月現在、もはやTech系の番組よりもよく聴いているかもしれない
  • この回とかめっちゃ面白い: https://hiromaeda.com/2020/02/02/layerx-sugoi/

ゼロトピック

その他

バイリンガルニュース

「初めてのGraphQL」を読んでサンプルアプリをTypeScript + React + typeorm + type-graphql + graphql-code-generatorで作ってみた

最近、GraphQLを勉強するために「初めてのGraphQL」という本を読んで、理解のために自分でも本の中で実装されているサンプルアプリを作ってみました。

初めてのGraphQL ―Webサービスを作って学ぶ新世代API

初めてのGraphQL ―Webサービスを作って学ぶ新世代API

作ったのがこれ: https://github.com/danimal141/learning-graphql-ts

ただ写経するだけだとしっかり理解できない + 面白くないので本は雑に読みつつ、色々と魔改造しながら作ったのでそのメモを残しておきます。

全体

  • 本のサンプルアプリはJavaScriptで書かれているが、全てTypeScriptで書いた
    • GraphQLがせっかく型システムを持っているのだから実装側も型がほしい
  • ディレクトリ構成はMonorepoで yarn workspaceを使ってバックエンドとフロントエンドのpackageを良い感じに管理できるようにした
  • npm-run-allを導入してコマンドをパラレルでコマンド一発で実行するようにした
    • バックエンド、フロントエンド、GraphQLのスキーマを元に型を吐いてくれるやつとか色々立ち上げるやつがいたので
  • lint-stagedhuskyを使って、コミット時にPrettierをかませるようにした
  • その他、ESLintなどは適当に入れた
  • テストはだるかったので書いていません、すみませんすみません

Backend

  • 基本的には本の中で実装されているみたいにexpressやapollo-server-expressを使い、playground上でGraphQLのリクエストを確認できる感じにした
    • 途中で面倒になったので 本の中で実装されているが実装できていないResolverとかあるかも
  • MongoではなくMySQLをDockerで立ち上げるようにした
  • 使ってみたかったのでtypeormtype-graphqlを導入してDBとGraphQLのスキーマ定義するようにした
    • type-graphqlが勝手にスキーマ吐いてくれるので良い
    • Spring Bootみたいになる
    • 後で気づいたが最近名前をちらほら聞くNest.jsとやらを使ってみても良かったのでは...?と思い始めている
  • あまりよく理解せずノリで使っていたが、途中でtypeormがMigration機能持っていることに気づき導入してみた (最初は synchronize: trueで乗り切っていた)

Frontend

  • create-react-appを使ってTypeScriptのアプリのベースを作った
    • 途中で面倒になったので作ったのはユーザ一覧表示とユーザログイン部分機能だけ
    • 本の中のReactはちょっと古い感じだったのでHooksを使いまくってモダンっぽくした
  • graphql-code-generatorを導入して、バックエンドでtype-graphqlが生成したスキーマを元に型ファイルを生成するようにした
  • クエリを投げまくるのを気にして、なるべくルートのコンポーネントがクエリを組み立ててリクエスト -> 子コンポーネントはフラグメントで欲しい情報を宣言し、レンダリングするみたいなのを意識した (Fragment Colocationと言う? 参照
    • 実際にはMutationでクエリの再フェッチがあったり、そもそも実装したコンポーネントがほぼ無いのであまりうまくできていない...
    • 仕事でNext.js + GraphQLを使った時にも意識したやり方だけど、pagesでクエリを組み立て、pagesを構成する各コンポーネントで必要な情報をFragmentで宣言するみたいにすると、リクエストも最低限になりまあまあキレイな設計になるのではないかと思っている (GraphQL歴2ヶ月の意見です)
  • ログインする所は初めてMutationをフロントエンドで実装したので若干悩んだ
    • 本の中ではMutationコンポーネントとか使ってたけど、Hooksで書き直した時にuseMutationがvariables空で呼ばれてしまってエラーになったりしてちょっとハマった

まとめ

  • GraphQLとTypeScriptは良い感じに型システムを使えるので相性かなり良さそう
  • typeormは悪くないけど、個人的には正直ActiveRecordのほうが早く実装できる
    • Railsヤメラレナイ...

参照