OIDCとは

「Googleでログイン」のようなソーシャルログインや、GitHub ActionsからAWSを操作するキーレス認証など、いろいろな場面で登場するのがOIDC(OpenID Connect)である。名前はよく聞くが、OAuthとの違いや裏側の仕組みを曖昧なまま使っていたので、改めて調べてまとめてみた。

この記事では、以下がわかるようにまとめている。

  • [What] OIDCとはなにか?OAuth 2.0と何が違うのか?
  • [Why] なぜOAuthだけでは認証に使えないのか?
  • [How] OIDCのフローはどう動くのか?実装には何が必要か?
  • [Security] よりセキュアなOIDCにするには?

OIDCとは?

OIDC(OpenID Connect)は、OAuth 2.0を拡張して「認証」の機能を追加した仕様である。

OpenID Foundationの公式ページでは、以下のように説明されている。

OpenID Connect is an interoperable authentication protocol based on the OAuth 2.0 framework of specifications.

一言でいうと「OAuth 2.0の仕組みに乗っかって、ユーザーが誰であるかを安全に確認できるようにしたプロトコル」である。

ポイントは、OAuth 2.0とOIDCで役割が異なること。

プロトコル役割答える質問
OAuth 2.0認可(Authorization)「この操作をする権限があるか?」
OIDC認証(Authentication)「あなたは誰か?」

OAuth 2.0は「あるアプリに、自分のリソースへのアクセスを許可する」ための仕組みであり、そこに「アクセスしてきたユーザーが誰か」を証明する機能はない。この穴を埋めるのがOIDCである。

なぜOAuthだけでは認証に使えないのか

「OAuthでアクセストークンを取得できた = 本人確認できた」と見なす実装は、一見動くが危険である。

OAuth 2.0のアクセストークンは「リソースにアクセスする許可証」であって、「誰に発行されたか」を検証する仕組みを持たない。そのため、例えば以下のような攻撃が成立してしまう。

  1. 攻撃者が悪意あるアプリを作り、被害者にOAuthでログインさせてアクセストークンを入手する。
  2. 攻撃者は、そのトークンを「OAuthを認証代わりに使っている別の正規アプリ」に送りつける。
  3. 正規アプリはトークンが有効であることしか確認しないため、攻撃者を被害者本人としてログインさせてしまう。

いわゆるトークン置き換え(token substitution)攻撃である。アクセストークンには「どのクライアント向けに発行されたか」「ユーザー認証がいつ行われたか」といった情報が含まれないことが根本原因。

OIDCはこの問題を、後述するIDトークンで解決している。

IDトークン

OIDCの中核となるのがIDトークンである。これは「ユーザーが、いつ、どこで認証されたか」を証明するデータで、JWT(JSON Web Token) 形式で発行される。

JWTは ヘッダー.ペイロード.署名 の3パートをそれぞれBase64URLエンコードして . で繋いだ文字列で、ペイロードには以下のようなクレーム(属性情報)が含まれる。

{
  "iss": "https://accounts.google.com",
  "sub": "10769150350006150715113082367",
  "aud": "my-client-id.apps.googleusercontent.com",
  "exp": 1784091000,
  "iat": 1784087400,
  "nonce": "n-0S6_WzA2Mj"
}

主要なクレームの意味は以下の通り。

  • iss(issuer): トークンの発行者。どのOpenID Providerが発行したか。
  • sub(subject): ユーザーの識別子。発行者内で一意。
  • aud(audience): トークンの発行先。どのクライアント向けに発行されたか。
  • exp / iat: 有効期限と発行時刻。
  • nonce: リプレイ攻撃対策のワンタイム値(後述)。

そして重要なのが、IDトークンには発行者による電子署名が付いている点。受け取った側は発行者の公開鍵で署名を検証することで、「このトークンは確かにこの発行者が発行したものであり、改ざんされていない」ことを確認できる。

先ほどのトークン置き換え攻撃も、aud を確認すれば「このトークンは自分のアプリ向けに発行されたものか?」を検証できるため防げる。アクセストークンが「許可証」なのに対して、IDトークンは「署名付きの身分証明書」 と捉えるとわかりやすいと思う。

登場人物

OIDCのフローには3つの登場人物がいる。

  1. エンドユーザー(End User): 認証される本人。
  2. RP(Relying Party): ユーザーを認証したいアプリケーション。OAuth 2.0でいうクライアント。
  3. OP(OpenID Provider): ユーザーを認証し、IDトークンを発行するサーバー。OAuth 2.0でいう認可サーバー。GoogleやLINEなどがこれにあたる。

OIDCのフロー

OIDCにはいくつかのフローがあるが、最も基本かつ推奨されるのが認可コードフロー(Authorization Code Flow) である。「Googleでログイン」を例に流れを追ってみる。

sequenceDiagram
    actor User as エンドユーザー
    participant RP as RP(アプリ)
    participant OP as OP(Google)

    User->>RP: (1) ログイン要求
    RP-->>User: (2) OPへリダイレクト(client_id, scope=openid, state, nonce等)
    User->>OP: (3) 認証(ID/パスワード等)&同意
    OP-->>User: (4) RPへリダイレクト(認可コード付き)
    User->>RP: 認可コードをRPへ
    RP->>OP: (5) 認可コード + client_secret
    OP-->>RP: (6) IDトークン + アクセストークン
    Note over RP: (7) IDトークンの署名・クレーム検証
    RP-->>User: (8) ログイン完了
  1. ユーザーがRPで「Googleでログイン」を押す。
  2. RPはユーザーをOPの認証画面へリダイレクトする。このとき scopeopenid を含めるのがOIDCの合図。
  3. ユーザーはOP上で認証(すでにログイン済みならスキップされる)し、RPへの情報提供に同意する。
  4. OPは認可コードという短命なコードを付けてユーザーをRPへリダイレクトする。
  5. RPはバックエンドから、認可コードと自身のクレデンシャル(client_secret)をOPのトークンエンドポイントへ送る。
  6. OPはIDトークン(とアクセストークン)を返す。
  7. RPはIDトークンの署名を検証し、iss / aud / exp / nonce などのクレームを確認する。
  8. 検証を通過すれば「このユーザーは確かにGoogleに認証された本人」と確定し、ログイン処理を完了する。

トークンがブラウザを経由せず、RPのバックエンドとOPの間で直接受け渡される(手順5〜6)のがこのフローのミソ。ブラウザに露出するのは短命で1回しか使えない認可コードだけなので、トークンの漏洩リスクを抑えられる。

なお、他にIDトークンをブラウザ経由で直接返すインプリシットフローなどもあるが、トークンがブラウザに露出するためセキュリティ上の理由から現在は非推奨とされている。基本的には認可コードフロー(+ 後述のPKCE)を使えばよい。

OAuthのフローとの違いはどこか?

見ての通り、OAuthの認可コードフローとほぼ同じである。違いは以下の2点だけ。

  • 認可リクエストの scopeopenid を含める(これがOIDCの合図)
  • トークン交換(手順6)のレスポンスに、アクセストークンと並んでIDトークンが一緒に返ってくる

つまり「OAuthのトークン交換のタイミングで、IDトークンも同時に発行される」という理解で正しい。OIDCがOAuth 2.0の拡張と呼ばれるのはこのためで、既存のフローに乗っかって、返ってくるものにIDトークンが1つ増えただけ、と見ることができる。

アクセストークンは何に使うのか?

では、ログイン目的なのに一緒に返ってくるアクセストークンには意味があるのか。

OIDCの文脈でのアクセストークンの主な用途は、OPが提供するUserInfoエンドポイントの呼び出しである。IDトークンに含まれない(あるいは含めなかった)ユーザーのプロフィール情報(名前、メールアドレス、アイコン画像など)を、このエンドポイントから取得できる。scopeprofileemail を追加しておくと、対応するクレームが取れるようになる。

  • 純粋にログインだけしたい場合: 必要な情報がIDトークンのクレームで足りているなら、アクセストークンは実際ほぼ出番がない。
  • 認証と認可を同時にやる場合: 「Googleでログインしつつ、カレンダーへのアクセス許可ももらう」ように scope にAPI権限も含めれば、アクセストークンはそのままGoogle Calendar APIを叩くための本来の(OAuthとしての)役割を持つ。

「認証だけならIDトークンが主役でアクセストークンは脇役、認可も兼ねるなら両方が主役」という整理でよいと思う。

ログイン完了後、「ログイン中」は何が保証するのか?

もう一つ重要なのが、手順(8)の「ログイン完了」の後の話。IDトークンはRPのバックエンドに渡って検証されたが、その後ブラウザには何が保存され、2回目以降のリクエストで「このユーザーはログイン中」と何が保証するのか?

答えは少し拍子抜けで、OIDCはそこには関与しない。OIDCが担当するのは「このユーザーは確かに本人である」と確認する瞬間まで。それ以降のログイン状態の維持は、RP自身の従来通りのセッション管理の仕事である。

  1. IDトークンの検証を通過したら、RPは自前のセッションを作る(セッションストアに保存、またはセッション情報を署名付きクッキーに焼く)。
  2. RPはセッションIDをクッキーSet-Cookie)としてブラウザに返す。ブラウザに保存されるのはこのRPのセッションクッキーであり、IDトークンそのものではない。
  3. 2回目以降のリクエストでは、ブラウザが自動送信するセッションクッキーだけで「ログイン中のユーザー」を判定する。IDトークンはもう登場しない。

IDトークンはログインの瞬間に1回検証されて役目を終える、使い捨ての身分証明書であり、セッションの維持に使い回すものではない(有効期限も数分〜1時間程度と短い)。パスワードログインで例えるなら、OIDCが置き換えたのは「パスワード照合」の部分だけで、その後の「セッションクッキーでログイン状態を維持する」部分は何も変わっていない、ということである。

ちなみにブラウザには、RPのセッションクッキーとは別にOP(Google)自身のセッションクッキーも保存されている。手順(3)で「すでにログイン済みならスキップされる」のはこのおかげで、複数のRPが同じOPを使っていれば、2つ目以降のRPへのログインは認証画面なしで完了する。これがOIDCによるシングルサインオン(SSO) の正体である。

実装には何が必要か?

OIDCを「使う」立場(RP側)であれば、自分で仕様をフル実装する必要はほとんどない。

  • OPとして使えるサービス: Google、Microsoft、LINEなどのIDプロバイダー。(なお「GitHubでログイン」は素のOAuthであり、GitHubはユーザーログイン用のOIDCを提供していない点に注意。)自前で用意するなら Keycloak や、マネージドサービスの Auth0Amazon Cognito など。
  • RP側のライブラリ: 各言語に認定ライブラリがある(OpenID Foundationの認定ライブラリ一覧)。IDトークンの署名検証やクレーム検証はライブラリに任せるのが安全。

また、OPは /.well-known/openid-configuration というエンドポイントで自身の設定情報(各エンドポイントのURL、公開鍵の取得先である jwks_uri など)を公開している(OpenID Connect Discovery)。ライブラリはこれを読んで自動で設定を組み立ててくれるため、RP側の実装は「OPのURLとクライアント情報を渡す」程度で済むことが多い。

よりセキュアなOIDCにするには?

OIDCを使っていても、検証を省略すると穴になる。押さえておくべきポイントを挙げる。

  • IDトークンの署名検証を必ず行う: OPの jwks_uri から公開鍵を取得して署名を検証する。「JWTをデコードして中身を読むだけ」では改ざんを検出できない。
  • iss / aud / exp を検証する: 想定した発行者か、自分のクライアント向けに発行されたものか、失効していないか。特に aud の検証を怠るとトークン置き換え攻撃を許してしまう。
  • state パラメータを使う: RPがリダイレクト時に付与するランダム値。戻ってきたときに一致を確認することで、CSRF攻撃を防ぐ。
  • nonce を使う: 認証リクエスト時に付与するランダム値がIDトークンのクレームにそのまま入って返ってくる。一致を確認することで、IDトークンのリプレイ(使い回し)を防ぐ。
  • PKCEを使う: 認可コードの横取り対策。リクエスト時にランダム値のハッシュ(code_challenge)を送り、コード交換時に元の値(code_verifier)を送ることで、「コードを横取りしても交換できない」状態にする。もともとはモバイルアプリなど client_secret を安全に保持できないクライアント向けの仕組みだが、現在はすべてのクライアントでの利用が推奨されている。

とはいえ、これらの大半は認定ライブラリを正しく使えば面倒を見てくれる。自前でJWTの検証ロジックを書かないのが一番の近道だと思う。

認証以外への広がり

OIDCの「署名付きでアイデンティティを証明できる」という性質は、人間のログイン以外にも応用されている。

代表例が、CI/CDにおけるワークロードアイデンティティである。GitHub ActionsがOPとしてジョブの身元(リポジトリ・ブランチなど)を証明するIDトークンを発行し、AWSやGCPがそれを検証して一時クレデンシャルを発行する、という使い方ができる。これにより長期的なシークレットの保管が不要になる。

この話は別記事「GitHub ActionsからOIDCでAWSを操作する仕組みを理解する」に詳しくまとめている。

まとめ

  • OIDCはOAuth 2.0に認証機能を追加した仕様。OAuthが「認可(権限があるか)」、OIDCが「認証(誰であるか)」を担う。
  • 中核はIDトークン(JWT)。発行者の署名付きで iss / sub / aud / exp などのクレームを運び、「誰が・いつ・どこで認証されたか」を証明する。
  • 基本は認可コードフロー。OAuthとの違いは「scopeopenid を含める」「トークン交換時にIDトークンが一緒に返る」の2点だけ。
  • ログイン後の状態維持はOIDCの仕事ではなく、RP自身のセッション管理(セッションクッキー) が担う。IDトークンはログインの瞬間に1回検証されて役目を終える。
  • セキュアに使うには、署名・クレームの検証、state / nonce / PKCEの利用が重要。ただし認定ライブラリに任せるのが基本。

参考文献

一番分かりやすい OpenID Connect の説明 - Qiita

How OpenID Connect Works - OpenID Foundation

Final: OpenID Connect Core 1.0 incorporating errata set 2

RFC 7636: Proof Key for Code Exchange by OAuth Public Clients