SPAにおける理想的な認可

こんにちは、[kura（倉林 雅）](https://twitter.com/kura_lab)です。

この記事は[Digital Identity技術勉強会 #iddance Advent Calendar 2022](https://qiita.com/advent-calendar/2022/iddance)の23日目の記事です。

前回、[Cookie・OAuth 2.0・OpenID Connectの目的別プロトコル選定](https://qiita.com/kura_lab/items/9159f092a57f26c290cf)において目的別での適切なプロトコルのお話をいたしました。
今回はその話に関連したSPAにおける認可についてご紹介します。

# なぜSaaSにおけるSPAではOpenID ConnectとID Tokenを用いるのか？

はじめに、プロトコル選定や設計・実装において多くの開発者の誤解に繋がっているのではないかと思われる仕様について触れたいと思います。

前回のお話では、[新規にSPAを提供する場合はOAuth 2.0を選択する](https://qiita.com/kura_lab/items/9159f092a57f26c290cf#%E6%96%B0%E8%A6%8F%E3%81%ABspa%E3%82%92%E6%8F%90%E4%BE%9B%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88%E3%81%AFoauth-20%E3%82%92%E9%81%B8%E6%8A%9E%E3%81%99%E3%82%8B)で解説したようにSPAでアプリケーションを提供する際には、OAuth 2.0による認可を行いフロントエンドのJavaScriptにAccess Tokenを発行する方法がよいと考えています。

```plantuml
@startuml
title 新規にSPAを提供する場合はOAuth 2.0を選択する\n
start
#Lime:if (\n1st Partyアプリを\n提供する？\n) then (はい)
-[#Green]->
  if (MPAで提供する？) then (はい)
    label alignment
    label alignment
    label alignment
    label alignment
    label cookie
    :Cookie;
    stop
  else (いいえ)
  -[#Green]->
    #Lime:if (SPAで提供する？) then (はい)
    -[#Green]->
      #Lime:if (\nすでにMPAを\n提供している？\n) then (はい)
        label alignment
        goto cookie
      else (いいえ)
        label alignment
        label oauth2
        #Lime:OAuth 2.0;
        stop
    endif
    else (いいえ)
      :ネイティブアプリで\n提供する;
      goto oauth2
    endif
  endif
else (いいえ)
  if (\n2nd/3rd Partyに\nAPIを提供する？\n) then (はい)
    label alignment
    label alignment
    label alignment
    goto oauth2
  else (いいえ)
    if (\nIdPとして2nd/3rd Partyに\nID連携を提供する？\n) then (はい)
      :OpenID Connect;
      stop
    else (いいえ)
      :RPとして2nd/3rd Partyの\nID連携/APIを受け入れる;
      stop
    endif
  endif
endif
@enduml
```

このユースケースの要件は以下にあります。

* SPAのJavaScriptがユーザーリソースにアクセス（APIアクセス）するためにAccess Tokenを取得できること
    * サーバーサイドでID管理し他社IdPとのID連携をするケースと異なり、このケースではID連携をする必要はないため、ID Tokenの検証やユーザー識別子の紐付けなどはSPA上では不要である

純粋にSPAではAPIへの「認可」ができれば機能を提供できるため、OAuth 2.0を用いればよいということになります。

ですが、SaaSによる認証・認可機能ではOAuth 2.0とAccess Tokenではなく、OpenID ConnectとID Token(JSON Web Token, JWT）で実現されている事例が見受けられます。

* [ID トークンの使用 -AWS Cognito-](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-id-token.html)
    * リソースサーバーにID Tokenでアクセスできる旨が記載されている
* [Control access to a REST API using Amazon Cognito user pools as authorizer](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html)
    * API GatewayでAuthorizerを設定することでID Tokenでアクセスも可能である旨が記載されている

AWS Cognitoの仕様をみると、User Poolで管理しているユーザーの制御において、アプリケーション上でのユーザー認証やリソースアクセスのためにOpenID Connectのインターフェースで発行したID Tokenを利用できるとなっているようです。
また、ドキュメントに従ってSPAを実装しようとするとAmplifyのSDKの利用も提案されており、Amplify内部でもID Tokenが用いられたリソースアクセスになっているようです。

これは推測ですが、User Poolのユーザー管理に関連する制御機能は「ユーザー認証を目的とするID Token」でも柔軟にできるようにしようという意図でこのような仕様となっているのだと想像しています。

基本的にID TokenはOpenID Connectを提供するOpenID Provider（OP）で行ったユーザー認証の結果をID連携先であるRelying Party（RP）へ伝える仕組みであり、ユーザーリソースのアクセスに用いる想定はされていません。

あくまでCognitoの独自仕様であるということを理解しておく必要があります。

※ この仕様に至った経緯がわかる記事や実際に仕様策定に携わった方で背景をご存知の方がいましたら、ぜひ共有していただけると幸いです。

Cognitoは認可エンドポイントの`scope`パラメーターで`openid`を指定することでOpenID ConnectのインターフェースでID Tokenを取得することができるわけですが、`openid`を指定しなければID Tokenを発行せずに、純粋なOAuth 2.0として利用することも可能です。
その場合には、[新規にSPAを提供する場合はOAuth 2.0を選択する](https://qiita.com/kura_lab/items/9159f092a57f26c290cf#%E6%96%B0%E8%A6%8F%E3%81%ABspa%E3%82%92%E6%8F%90%E4%BE%9B%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88%E3%81%AFoauth-20%E3%82%92%E9%81%B8%E6%8A%9E%E3%81%99%E3%82%8B)の通りの実装ができると考えています。

# 「JWT=リソースアクセスのためのフォーマット」ということではない

本筋からは少し外れますが、JWTフォーマットのAccess Tokenについても触れておきます。

CognitoやAuth0ではAccess TokenもID Tokenと同じJWTのフォーマットになっています。

* [アクセストークンの使用 -AWS Cognito-](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-access-token.html)
* [Access Tokens -Auth0-](https://auth0.com/docs/secure/tokens/access-tokens)

これはCognitoやAuth0を利用するアプリケーションが提供するAPIの内部でAccess Tokenの検証をしやすいように標準フォーマットを利用しているという認識です。

これはリソースアクセスに使用するAccess TokenがJWTフォーマットであるということであり、JWTフォーマットであるID Tokenもリソースアクセスに使用することを想定している、ということではないので注意しましょう。

ID TokenとAccess Tokenの用途については以下のAuth0の記事で整理されているためご参照ください。

* [ID Token and Access Token: What's the Difference? -Auth0-](https://auth0.com/blog/id-token-access-token-what-is-the-difference/)

# SPAの認可ではメモリ上でAccess Tokenを管理することが重要

混乱を招いていると思われる仕様について整理できたので、改めてここからがSPAにおける認可のお話です。

現在、IETFのoauth WGでブラウザーベースのOAuth 2.0の実装についてベストプラクティスの策定が続いています。

* [OAuth 2.0 for Browser-Based Apps draft 12](https://datatracker.ietf.org/doc/draft-ietf-oauth-browser-based-apps/)

Access Tokenをローカルストレージに保存するとXSSによるAccess Token漏えいの脆弱性のリスクが伴います。常にXSS対策が継続されていればよいですが、可能であればリスクを回避すべきです。

このdraftで定義されているリスク回避方法は、ローカルストレージに保存せず、Service Workerを用いてメモリ上でAccess Tokenを管理する方法です。
以下は、SPAのOAuth 2.0をService Workerで実装したフローのシーケンス図になります。

```plantuml
@startuml OAuth 2.0 Authorization Code Grant
autonumber
actor "Resource\nOwner" as RO
participant "SPA\nMain Thread" as MT
participant "SPA\nService Worker" as SW
participant "Authorization\nServer" as AS
participant "Resource\nServer" as RS

RO -> MT:
MT -> SW:
SW -> AS: Authorization Request\nClient Identifier & Redirection URI\n& State
AS -> SW: Authorization Code & State
SW -> SW: Validates State
SW -> AS: Access Token Request\nAuthorization Code & Redirection URI
AS -> SW: Access Token
SW -> SW: Store Access Token only in memory
SW -> RS: Protected Resource Request\nAccess Token
RS -> SW: Protected Resource
SW -> MT: Response
MT -> RO:
@enduml
```

アプリケーションのメインスレッドから認可コードやAccess Tokenの管理を分離することで、XSSのリスクを回避するということになります。

Auth0やmercariにおいてはWeb Workerを利用した実装が提案、実現されているようです。各社、ユーザー認証の方式には違いがあるようですが、Access Tokenの管理をメインスレッドから分離するという方式については共通していそうです。

* [Token Storage -Auth0-](https://auth0.com/docs/secure/security-guidance/data-security/token-storage)
* [Building secure web apps using Web Workers -mercari-](https://engineering.mercari.com/en/blog/entry/20220930-building-secure-apps-using-web-workers/)

基本的なOAuth 2.0 / OpenID Connectのシーケンスについては以下にまとめているので、SPAのシーケンスとの違いを確認してみてください。

https://qiita.com/kura_lab/items/812a62b5aa3427bdb49d

# 最後に

今回は、OpenID ConnectとID TokenによるリソースアクセスやJWTフォーマットのAccess Tokenなどで起こりがちな誤解をご説明し、改めてSPAにおける理想的な認可についてご紹介しました。

SPAの認可については方針が確認できたので、次の機会にはユーザー認証も含めてお話ができたらと思います。

最後まで読んでいただきありがとうございました。よいお年を！