Serverless Operations, inc

>_cd /blog/id_h-4vosjh2ec

title

モダンアプリケーションにおけるユーザー認証・認可の実装のために考慮しておきたいこと

summary

この記事では、「モダンアプリケーション」の定義として、AWSマネージドサービスを積極的に利用する「クラウドネイティブな構成のバックエンドアプリケーション」と「ウェブクライアント」の開発を想定しており、その中でも固定費用が発生しないサーバーレスアーキテクチャを導入した構成でのユーザー認証認可において考慮しておきたい点をまとめました。

2024年現在、ユーザー認証認可に関する様々なソリューションがありますが、未だに対処に困るような場面があります。この記事では、そのような部分にフォーカスし、モダンアプリケーション開発の脈略で解決に向けたアプローチをお伝えします。

また、この記事よりさらに踏み込んだ内容と、詳細な設計パターンおよび実践的なプラクティスについて、ServerlessDays Tokyo 2024 (https://tokyo.serverlessdays.io/) にて発表させていただく予定です。気になる方は是非足を運んでいただき、ご質問などあればお気軽にお話いただければと思います。

ID Provider 選定

OAuth2 が普及して以来、伝統的なセッション機構ではなく、WebAPI としてリソースサーバーを構築し、別途ユーザー認証認可のための ID Provider と連携する構成が一般化しています。ID Provider は、アプリの性質と付帯環境を考慮し、OAuth2 の拡張仕様である Open ID Connect をサポートするサービスを選定すると良いです。以下、ID Provider のサービスをいくつかピックアップして紹介します。

ID Provider

内容

Auth0

  • モダンアプリケーションの代表格な IDaaS (Identity as a service)
  • auth0-spa-js, next-auth0 など周辺ライブラリと生態系が整っている
  • 一般的な観点、もしくは、外部 3rd party 製品利用可能な場合、最もおすすめの選択肢

Amazon Cognito

  • AWS マネージドサービス
  • システム構成を AWS に固めたい場合、様々な AWS サービスと直接連携場合はおすすめ
  • AWS サポートが受けられる

Google

  • 直接連携の他、Auth0 や Cognito を挟んでソーシャルログイン連携も可能なため、柔軟に使える
  • 社内の IT 基盤が Google Workspaces で、社内向けアプリを開発する場合は Google に直接連携して利用すると便利

Microsoft Entra ID (旧 Azure AD)

  • Microsoft ID を活用または Azure AD 移行済みの場合、便利で開発経験も良い
  • 専用 Authenticator アプリとパスワードレス認証が使える
  • 社内の IT 基盤が Microsoft または Azureで、社内向けアプリを開発する場合はおすすめ

他にも、Authlete や Keycloak など様々な ID と認証認可のサービスがあります。これらの特性をある程度把握した上で、長期的な観点でユーザー基盤としてどのように活用していくかを事前に検討しておくと良いです。

トークンライフサイクルの考え方

トークンライフサイクルは戦略的な判断が必要になることがあります。例えば、日常的に利用するサービスの場合、短い周期で頻繁にログインを求めてしまうことは一般的に望ましくありません。どのようなユーザー経験を与え、またセキュリティに関してどの水準までを考慮するかといった狙いを明確に定める必要があります。

ユーザー経験を考慮したアプローチ検討

例えば、ログインを行う頻度について、以下のようなパターンが挙げられます。

  • 一度ログインした後、毎日使っていれば再ログインを求めない
  • 一度ログインした後、週に1回/月に1回でも使っていれば、再ログインを求めない
  • 一度ログインした後、1ヶ月以内は再ログインせず利用できるが、月に1回は必ず再ログインが必要
  • ブラウザを開くたびに再ログインを求める

低頻度かつ定常的な作業で利用されるアプリに関しては、リフレッシュ機構を意図的に作らないことで、セキュリティ観点での考慮事項を減らし、開発・運用効率を取る選択肢が考えられます。一方で、業務や生活の中で常時利用されるアプリでは、高頻度に再ログインを求めることは難しく、ユーザー体験を配慮しつつセキュアにセッションを維持する仕組みを考える必要があります。

また、アプリ単体ではなく複数のサービスプロバイダーへの SSO (Single Sign-On) を実現したい場合や、Okta, AWS IAM Identity Center, Akamai EAA, Microsoft Entra SSO など様々なソリューションを利用してプラットフォームによる共通体験を提供したいという場合もあります。事前に要件と提供したい体験を整理し、開発着手前のタイミングで仕様の落とし所の目安を決めておく過程があると良いです。

認証・認可トークンの種類

従来は「セッション」と言われ、ユーザー認証を行った後、Cookie を使ってステートフルな値を利用する形が一般的でした。しかし OAuth2 の普及により、主に以下2種類のトークンを利用することが一般的になってきています。

  • アクセストークン(Access Token):API エンドポイント(リソースサーバー)へのアクセス
  • リフレッシュトークン(Refresh Token):新しいアクセストークンの発行

さらに、Open ID Connect をサポートする ID Provider は、以下のトークンも合わせて発行します。

  • ID トークン(ID Token):認証済みユーザーであることを証明するトークン

Open ID Connect に準拠した ID Provider の場合、別の場所でも再度ユーザー認証をすることなく ID 連携が可能になる仕組みがあります。モダンアプリケーション開発では、この Open ID Connect の仕様に準拠した ID Provider を利用することが基本となっており、上記3つのトークンの用途についてある程度認識しておく必要があります。

Open ID Connect の理解

Open ID Connect (OIDC) は、認可の仕組みをまとめた OAuth2 に、ID 基盤として認証統合機構を追加した拡張仕様です。Open ID Connect をサポートする ID Provider にログインすると「ID トークン」が発行され、ID トークンの検証を行うことで認証済みユーザーであることが分かります。アプリ側は、この ID トークンを検証し、API への認可(アクセストークンの発行)を行った上で、アクセスを受け入れる流れになります。

また、OIDC をサポートする ID Provider はアクセストークンの発行(認可サーバーの役割)も兼ねていることが多いため、通常はアクセストークン、リフレッシュトークン、ID トークンの3点がセットで発行されます。

複数のアプリケーションに対して共通の認証基盤を構成する場合、この ID トークン を連携して活用することになります。また、この ID トークンの仕様として有名なのが、 JWT (JSON Web Token) です。

JWT(JSON Web Token)のステートレス性

JWT は ID トークンの仕様として有名ですが、アクセストークンにも活用されています。クローズドなサービスの開発では独自のJWT 発行・検証機構を検討されることもあるかと思います。

この JWT を利用する上で知っておくべきことは、「ステートレス性」です。サーバー側で状態を保持する「セッション」とは異なり、JWT は発行された状態をサーバー側で保持しない想定で作られています。ただし有効期限は付いているため、揮発性のあるデータとして扱う想定を持つことがポイントになります。後述するトークンの無効化についても、JWT の状態を管理する方法ではなく、ブラックリストのアプローチが有効です。

標準の Optional クレームとして jti (JWT ID)が用意されていますが、ステートレス性を失うと JWT を利用する優位性が薄れてしまうため、 jti を状態管理に用いることはおすすめしません。

トークンライフサイクル管理戦略

ここまでの流れで、各トークンの用途と JWT の特性などを踏まえ、「ユーザー経験を考慮したアプローチ」をどのように実現するかについて考えてみます。ID トークンは原則クライアントから送信されるものではないため、アクセストークンとリフレッシュトークンの用例を以下のように整理します。

パターン

アクセストークンの利用

リフレッシュトークンの利用

リフレッシュトークンのローテーション

考慮事項

ユースケースの例

①アクセストークンのみ利用

×

×

  • ブラウザを開くたびに再ログインが求められる
  • 利用頻度は月に1回程度

②アクセストークンとリフレッシュトークンを利用する(ローテーション無し)

×

  • 一度ログインした後、再ログインせず利用できるが、月に1回は必ず再ログインが必要
  • 利用頻度は月に数回程度

③アクセストークンとリフレッシュトークンを利用する(ローテーションあり)

  • 一度ログインした後、再ログインせず利用できるが、一定期間(1〜数ヶ月)過ぎたら再ログインが必要
  • 基本的に毎日/毎週使っていれば再ログインを求めない

ポイントとなるのは、「リフレッシュトークン」の使用有無・有効期限・ローテーション有無です。

アクセストークンについては、短い有効期限(15分〜1時間)を指定することになります。またユーザーにどの頻度で再ログインを求めるかについては、リフレッシュトークンの有効期限によって決まりますので、ここにフォーカスする必要があります。アクセストークンの仕様として JWT を利用することも多くなってきている中、ステートレスなトークンは揮発する前提で作られているため、DBなど永続ストレージに保管したり、状態を管理するようなことはおすすめしません。

しかし、リフレッシュトークンについては、新しいアクセストークンを取得できる点もさることながら、有効期限を長めに設定するため(数時間〜数ヶ月)、漏洩しないように注意が必要です。特に多くの情報がブラウザによってオープンになりやすいウェブクライアントでは、リスクを軽減しながらリフレッシュトークンを扱うための工夫が必要です。そのため主要 ID プロバイダーはリフレッシュトークンのローテーション機能を提供しており、一度利用したリフレッシュトークンは再利用せず、都度新しいものを発行する形になります。基本的にはローテーションを有効にしておくことが推奨されます。

トークンの保存場所

モダンアプリケーションでのアクセストークンとリフレッシュトークンの保存場所については、長い間議論されてきました。フロントエンドアプリケーションを SPA(Single Page Application)と SSR(Server-side Rendering)という括りで区別してみると、まず SSR の場合、考慮事項は比較的少なくなります。基本的にサーバーサイドで Token API によるトークン発行を行い、トークン情報は Cookie を経由して利用できます。NextAuthAuth.js などのライブラリも同様に扱います。

なぜ Cookie 経由になるかについては、 HttpOnly 属性の有無です。名前からは想像しにくいですが、 HttpOnly 属性のついた Cookie は、JavaScript を利用してアクセスすることができません。よって、XSS(Cross-site scripting)による漏洩リスクが軽減される効果があります。同じ理由で、 localStorage など Web Storage にトークンを保存することは、XSS によって簡単にアクセスされてしまうので推奨されないと言われています。

※「Secure」属性にもチェックを入れておくことで、HTTPS 通信でのみ有効になります。「HttpOnly」と混同されることがよくありますが、Secure 属性のみでは JavaScript からアクセスできてしまうので要注意です。

この HttpOnly 属性がついていると、JavaScript ではアクセスできないため、サーバー側からフロントへ Set-Cookie ヘッダーを指定してのみトークンを渡す形になります。モダンアプリケーションと言いつつも、ここは「セッション」時代と同じやり方になってしまいますが、このような経緯から SSR (Server-side Rendering)にすると比較的トークン類が扱いやすい環境になります。

※ SSR(Next.js - NextAuth)でのセッション(トークン)更新処理の例

問題は、サーバー側の仕組みを持たない SPA(Single Page Application)でのケアをどうするかです。常にブラウザから API エンドポイントをステートレスに呼び出す必要がある SPA は、認可サーバーが HttpOnly 属性をクロスオリジンで対応してくれなければ、XSS から安全にトークンを扱う方法がないように見えます。しかし、JavaScript でトークンにアクセスできなければ、Bearer トークンとして API のエンドポイントにトークンを指定することができません。またクロスオリジンで HttpOnly 付き Cookie を扱うには、同じドメインを指定し、厳格に CORS を適用しなければなりません( Origin "*" が許容されません )。つまり、SPA と 外部 ID プロバイダーを利用する時点で、HttpOnly 属性の扱いが難しい環境になります。

SPA 構成でのトークンの保存と扱い方

まずは、アクセストークンとリフレッシュトークンを分離して考える必要があります。アクセストークンはステートレスに扱う必要があるのと、有効期限を短めに設定するため、揮発する性質を持ちます。また OAuth2 の仕様ではリソースサーバーのエンドポイントに Bearer トークンとして Authorization ヘッダーにトークンを指定することになっており、HttpOnly 付き Cookie で扱うことが現実的ではない場合もあります。このような付帯条件を鑑みて、SPA 構成であれば、インメモリストアや WebStorage を利用するという判断も実際に行われています(※ AWS Amplify に関してもデフォルトでは WebStorage を利用するようになっています。とはいえ、これで大丈夫 or 安心というわけではありません)。

また、アクセストークンの置き場所に関して、Web Storage は避け、インメモリストアを利用した方が良いと言われています。しかし JavaScript ライブラリに脆弱性が紛れ込んでいる場合、いずれの方法でもリスクはゼロにはできません。揮発性のあるアクセストークンに対しては、XSS を防ぐ対応を最優先事項として取り組む方が効果的になります。

一方で、リフレッシュトークンに関しては慎重なアプローチが必要です。アクセストークンよりも有効期限が長く、Token API を呼出すことで新しいアクセストークンが取得できてしまうため、ローテーションを有効にし、 HttpOnly 付き Cookie で扱うことをおすすめします。しかし、SPA 構成ではフロントエンド「サーバー」が扱えない環境が多く、工夫が必要になります。

Auth0 の SDK は、SPA 構成の場合でも、独自の方法でリフレッシュを行なってくれるので本項の問題は解決されています。一方、Cognito の場合( amplify-js を含め)このような方法はサポートされていないため、以下のような構成を検討すると良いです。

HttpOnly 付き Cookie の送受信には、同じドメインを適用し、も厳格に CORS を適用する必要がある点に注意してください( Origin "*" が許容されません )

また、リフレッシュトークン自体をサポートしない ID プロバイダーの製品を利用するなど、何らかの制約がある中で開発しなければならない場合があります。これに関しては、iframe と ID Provider 側のユーザー認証画面のセッションを利用する形でアクセストークンの再取得が可能になる、サイレント認証というテクニックがあります。

このように、SPA では、Auth0 を利用する場合を除き、リフレッシュトークンの扱いについてどうするかの方針を予め決めておくと良いです。

発行済みトークンの無効化(Revocation)

モダンアプリケーション開発で利用される ID Provider は、Auth0 を筆頭に、アクセストークンの無効化方法は提供されず、リフレッシュトークンの無効化のみサポートしています。リフレッシュトークンは長めの有効期限を持ち、ステートフルな管理が必要になるためです。また、リフレッシュトークンのローテーションを有効にすることで、使用済みのトークンを自動的に無効化することができます。

もし利用している ID Provider が無効化をサポートしていない場合や、アクセストークンも無効化できるようにしたい場合、ブラックリスト方式が有効です。AWS に馴染みのある方は、AWS 認証情報を無効化する方法が参考になれます。

※ AWS STS等で発行した一時的な AWS 認証情報もステートレスなため、過去に作成されたトークンを無効化するポリシーを適用するといったアプローチになります。
(参考:https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_credentials_temp_control-access_disable-perms.html

参考

この記事の内容と関連して、背景や詳細が気になる方は、以下の記事も合わせてチェックしてみてください。

まとめ

現場では主にリフレッシュトークンの扱いに悩まされることがメインになるかと思います。これが理由でフロントエンドは SSR を選択するケースもあると思いますが、開発全体の視点で見れば、SPA はサーバーレス構成ができ、インフラとサーバーレンダリングの考慮事項がなくなるため、長所も多いです。この記事がみなさんの判断材料の一つになれば幸いです。

この記事に関する内容を含め、モダンアプリケーション開発全般において気になる点やサポートが必要な場合は、お気軽にお問い合わせください。

Written by
COO

金 仙優

Sonu Kim

  • Facebook->
  • X->
  • GitHub->

Share

Facebook->X->
Back
to list
<-