Cookie・OAuth 2.0・OpenID Connectの目的別プロトコル選定


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

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

自社アプリケーションやAPIの提供に伴いCookie、OAuth 2.0、OpenID Connectなどどの仕組みを選択すればよいのか迷うことがあると思います。
今回は、それぞれのユースケースにおいて適切なプロトコルの選定の考え方についてご紹介します。

# プロトコルを選択する前に

どのプロトコルを選択するのか検討することがあると思いますが、プロトコルという手段からではなくまずは何をやりたいのかという目的を確認することが重要です。

目的を整理するにあたって以下の観点が挙げられます。

- アプリケーション・APIの立ち位置
  - 1st Party
  - 2nd/3rd Party
- アプリケーションの構成
  - MPA（Multi-Page Application）
  - SPA（Single-Page Application）
  - ネイティブアプリ
- 提供・連携するもの
  - アプリケーション
  - API
  - ID連携

何をやりたいのか明確にすることで正しいプロトコルの選定ができると考えています。

以下は目的別にプロトコルを振り分けたフローチャートになります。
このフローチャートに沿って解説をしていきます。

```plantuml
@startuml
title あなたがやりたいことは何ですか？\n
start
if (\n1st Partyアプリを\n提供する？\n) then (はい)
  if (MPAで提供する？) then (はい)
    label alignment
    label alignment
    label alignment
    label alignment
    label cookie
    :Cookie;
    stop
  else (いいえ)
    if (SPAで提供する？) then (はい)
      if (\nすでにMPAを\n提供している？\n) then (はい)
        label alignment
        goto cookie
      else (いいえ)
        label alignment
        label oauth2
        :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
```

# 自社アプリケーションを提供する

まずは自社アプリケーションを提供するケースについてです。いわゆる1st Partyアプリの提供です。
1st PartyアプリのWebアプリケーションやiOS/Androidのネイティブアプリケーションの提供で選択するプロトコルをみていきましょう。

## 1st PartyアプリをMPAで提供する場合はCookieが良い

1st PartyアプリをMPA（Multi-Page Application）として提供する場合には、基本的にCookieによってユーザーリソースにアクセスさせる方法がよいでしょう。

```plantuml
@startuml
title 1st PartyアプリをMPAで提供する場合はCookieが良い\n
start
#Lime:if (\n1st Partyアプリを\n提供する？\n) then (はい)
  -[#Green]->
  #Lime:if (MPAで提供する？) then (はい)
    label alignment
    label alignment
    label alignment
    label alignment
    label cookie
    #Lime:Cookie;
    stop
  else (いいえ)
    if (SPAで提供する？) then (はい)
      if (\nすでにMPAを\n提供している？\n) then (はい)
        label alignment
        goto cookie
      else (いいえ)
        label alignment
        label oauth2
        :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
```

OAuth 2.0やOpenID Connectを選択せず、一般的なWebアプリケーションで行う処理をします。

1. ID基盤のログイン機能でユーザーを認証しCookieを発行する
1. ブラウザーから送信されるCookieヘッダーをサーバーサイドで取得しユーザーを識別する
1. ユーザー情報の更新や参照処理をしてサーバーサイドでレンダリングしてレスポンスを返却する

変にOAuth 2.0やOpenID Connectを導入すると複雑な構成になるため、シンプルなWebアプリケーションの仕組みを用いるとよいでしょう。

## MPAを提供していて追加でSPAを提供する場合もCookieが良い

1st PartyアプリでSPAを提供する場合でも、すでにMPAとCookieの仕組みを提供している際にはCookieを選択するのがよいでしょう。

CookieとOAuth 2.0のAccess Tokenの2つの運用はコストになるため、将来的にMPAとCookieの仕組みを廃止し完全にSPAへ移行する方針がない場合には、SPAとCookieの構成を選択すべきだと考えます。

```plantuml
@startuml
title MPAを提供していて追加でSPAを提供する場合もCookieが良い\n
start
#Lime:if (\n1st Partyアプリを\n提供する？\n) then (はい)
-[#Green]->
  if (MPAで提供する？) then (はい)
    label alignment
    label alignment
    label alignment
    label alignment
    label cookie
    #Lime:Cookie;
    stop
  else (いいえ)
  -[#Green]->
    #Lime:if (SPAで提供する？) then (はい)
      #Lime:if (\nすでにMPAを\n提供している？\n) then (はい)
        label alignment
        goto cookie
      else (いいえ)
        label alignment
        label oauth2
        :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
```

1. ID基盤のログイン機能でユーザーを認証しCookieを発行する
1. フロントエンドのフレームワークやXHRなどから送信されるCookieヘッダーをAPIサーバーで取得しユーザーを識別する
1. ユーザー情報の更新や参照処理をしてAPIレスポンスを返却する

SPAのAPIなのにCookieを用いていてレガシーな感じがするかもしれませんが、すでにMAPとCookieの仕組みを運用している場合には無理にコストをかけてOAuth 2.0の導入はする必要はないと考えます。

## 新規にSPAを提供する場合はOAuth 2.0を選択する

新規サービスの立ち上げで、新たにSPAを提供する場合にはOAuth 2.0を選択するのがよいでしょう。

また、提供しているサービスの歴史が長いと技術的負債の返済のために、MPAからモダンなSPA（Single-Page Application）に徐々に移行していくケースにおいてもOAuth 2.0を提供するとよいでしょう。

```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
```

OAuth 2.0のフローでAccess Tokenを発行し、フロントエンドからAccess TokenでAPIをリクエストする処理をします。

1. ID基盤のログイン機能でユーザーを認証し、OAuth 2.0の認可サーバーでAccess Tokenを発行する
1. フロントエンドのフレームワークやJavaScriptから送信されるBearerヘッダーのAccess TokenをAPIサーバーで取得しユーザーを識別する
1. ユーザー情報の更新や参照処理をしてAPIレスポンスを返却する

これはSPAにおいては一般的な構成になるかと思います。
Access TokenやRefresh Tokenの安全な保存方法やXSS（Cross-Site Scripting）対策で悩むこともあると思いますが、そのあたりについては別の機会に解説したいと思います。

## ネイティブアプリを提供する場合はOAuth 2.0を選択する

iOSやAndroidといったネイティブアプリケーションを提供する場合にはOAuth 2.0を選択するのがよいでしょう。

```plantuml
@startuml
title ネイティブアプリを提供する場合は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 (はい)
      if (\nすでにMPAを\n提供している？\n) then (はい)
        label alignment
        goto cookie
      else (いいえ)
        label alignment
        label oauth2
        #Lime:OAuth 2.0;
        stop
    endif
    else (いいえ)
    -[#Green]->
      #Lime:ネイティブアプリで\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
```

OAuth 2.0のフローでAccess Tokenを発行し、ネイティブアプリのクライアントからAccess TokenでAPIをリクエストする処理をします。

1. ID基盤のログイン機能でユーザーを認証し、OAuth 2.0の認可サーバーでAccess Tokenを発行する
1. ネイティブアプリのクライアントから送信されるBearerヘッダーのAccess TokenをAPIサーバーで取得しユーザーを識別する
1. ユーザー情報の更新や参照処理をしてAPIレスポンスを返却する

これもネイティブアプリにおいては一般的な構成になるかと思います。
ベストプラクティスが定義されている[OAuth 2.0 for Native Apps](https://www.rfc-editor.org/rfc/rfc8252)に従うのがよいでしょう。

また、金融系のネイティブアプリでより強固なセキュリティが求められる場合には、[FAPI](https://fapi.openid.net/)のプロトコルでOpenID Connectの拡張を選択するケースもありますが、そのあたりの深掘りは別の機会にします。

# 2nd/3rd PartyにAPIを提供する場合はOAuth 2.0を選択する

次にグループ企業（2nd Party）やパートナー企業（3rd Party）にAPIを提供するケースについてです。

自社で管理するユーザーリソースの提供になるため、認可プロトコルであるOAuth 2.0を選択するのがよいでしょう。

```plantuml
@startuml
title 2nd/3rd PartyにAPIを提供する場合はOAuth 2.0を選択する\n
start
if (\n1st Partyアプリを\n提供する？\n) then (はい)
  if (MPAで提供する？) then (はい)
    label alignment
    label alignment
    label alignment
    label alignment
    label cookie
    :Cookie;
    stop
  else (いいえ)
    if (SPAで提供する？) then (はい)
      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 (いいえ)
-[#Green]->
  #Lime: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
```

OAuth 2.0のフローでAccess Tokenを発行し、2nd/3rd PartyのクライアントからAccess TokenでAPIをリクエストする処理をします。

1. ID基盤のログイン機能でユーザーを認証し、OAuth 2.0の認可サーバーでAccess Tokenを発行する
1. 2nd/3rd Partyのクライアントから送信されるBearerヘッダーのAccess TokenをAPIサーバーで取得しユーザーを識別する
1. ユーザー情報の更新や参照処理をしてAPIレスポンスを返却する

これもAPI提供のユースケースにおいては一般的な構成になるかと思います。

前述同様に、金融系のより強固なセキュリティが求められる場合には、[FAPI](https://fapi.openid.net/)のプロトコルでOpenID Connectの拡張を選択するケースもありますが、そのあたりの深掘りは別の機会にします。

# IdPとして2nd/3rd PartyにID連携を提供する場合はOpenID Connectを選択する

自社で管理しているID基盤の活用としてID連携があります。続いては2nd/3rd PartyにID連携を提供するケースについてです。

IdPとして2nd/3rd PartyにID連携を提供する場合にはOpenID Connectを選択するのがよいでしょう。

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

OpenID ConnectのフローでID TokenとAccess Tokenを発行し、2nd/3rd PartyのクライアントでID Tokenを検証し、その後Access TokenでAPIをリクエストする処理をします。

1. ID基盤のログイン機能でユーザーを認証し、OpenID Connectの認証・認可サーバーでID TokenとAccess Tokenを発行する
1. 2nd/3rd PartyのクライアントはID Tokenを検証し、ユーザー識別子を紐付ける
1. クライアントから送信されるBearerヘッダーのAccess TokenをUserInfoエンドポイント（またはAPIサーバー）で取得しユーザーを識別する
1. ユーザー情報の更新や参照処理をしてUserInfoエンドポイント（API）レスポンスを返却する

これもID連携のユースケースにおいては一般的な構成になるかと思います。

# RP（Client）として2nd/3rd PartyのID連携/APIを受け入れる場合

最後に、RP（Client）として2nd/3rd PartyのID連携/APIを受け入れる場合ですが、こちらは提供している自社アプリケーションの構成によってさまざまなパターンがあるため、次回に詳しく説明しようと思います。

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

# 最後に

今回は1st Party、2nd/3rd Party、MPAやSPAやネイティブアプリ、APIやID連携といった観点で目的別に選択すべきプロトコルを整理しました。
まず目的を整理することで最適なプロトコルが選定でき、アプリケーションの構成がよりシンプルになると思います。
サービスや技術的制約によってはご紹介したものに当てはまらないケースもあるかと思いますが、ご参考になったら幸いです。

次回はもう少し踏み込んだユースケースについてお話しできたらと思いますので、お楽しみにしていてください。