TT Lab
はじめる
学ぶ 学習パス コース

企業認証の連携

OIDCとSAML — 何を検証すれば安全なのか

TT Labで続きを見る

一言でいうと

OIDCでもSAMLでも、安全を作るのはプロトコルではなく、受信側の検証項目の一覧です。署名・iss・aud・exp・nonceのうち1つでも抜けると、その穴から入り込まれる可能性があります。

なぜこれが問題なのか

連携は、検証を抜かしても正常なフローでは完璧に動作します。ログインができて、画面が表示され、ユーザー名も正しく出ます。そのため、開発中も、結合テストでも、何の兆候もありません。

抜けた検証が明らかになる時点は2つだけです。セキュリティ診断を受けるときか、事故が起きたときです。署名を見なければ、誰でもペイロードを書き換えて任意のユーザーとして入れますし、audを見なければ、別のクライアント向けに発行されたトークンが、こちらのアプリで通用します。これは実装の難しさの問題ではなく、一覧を知っているかどうかの問題です。

OIDC Authorization Codeフロー

ブラウザーがあるWebアプリケーションの標準的なフローです。5つのステップです。

1. 사용자가 우리 앱의 보호된 페이지 요청
2. 앱 → 브라우저를 IdP 로 리다이렉트
   GET /authorize?response_type=code&client_id=labhub-web
                 &redirect_uri=https://app.example.com/callback
                 &scope=openid profile email
                 &state=<임의값>&nonce=<임의값>
3. IdP 가 로그인 처리 후 브라우저를 우리 앱으로 리다이렉트
   302 Location: https://app.example.com/callback?code=<인가코드>&state=<임의값>
4. 앱(서버)이 백채널로 토큰 교환
   POST /token  grant_type=authorization_code&code=...&redirect_uri=...
   → { access_token, id_token, refresh_token, expires_in }
5. 앱이 id_token 을 검증하고 세션을 만든다

このコードブロックの韓国語は、5つのステップを示しています。ユーザーが私たちのアプリの保護されたページを要求する、アプリがブラウザーをIdPへリダイレクトする(山括弧の中の韓国語のプレースホルダーはランダムな値)、IdPがログイン処理のあとブラウザーを私たちのアプリへリダイレクトする(山括弧の中の韓国語のプレースホルダーは認可コードとランダムな値)、アプリ(サーバー)がバックチャネルでトークンを交換する、アプリがid_tokenを検証してセッションを作る、です。

なぜ一度コードを経由するのか。トークンをブラウザーのURLで直接渡すと、ブラウザーの履歴・リファラー・ログにトークンが残ります。認可コードは使い捨てで寿命が短く、実際のトークンはサーバー対サーバー(バックチャネル)で受け取ります。だから安全です。

stateとnonceは役割が違います。よく混同されます。

IDトークンで必ず検証すること

IDトークンはJWTです。헤더.페이로드.서명(韓国語で順にヘッダー、ペイロード、署名を意味します)の3つの部分がドットでつながっていて、それぞれbase64urlです。デコードは誰でもできます。署名の検証が安全のすべてです。

検証項目 行わないと何が起きるか
署名 誰でもペイロードを書き換えて、管理者としてログインできます
iss(発行者) 攻撃者が自分のIdPで発行したトークンが通ります
aud(対象) 別のアプリ向けに発行されたトークンが通ります(トークン置換)
exp(有効期限) 期限切れのトークンが永遠に有効になります
nonce 以前のトークンが再利用されます
alg alg: noneやアルゴリズム混同攻撃が可能になります

最後の行が有名な落とし穴です。JWTライブラリがトークンが自ら申告したアルゴリズムを信じて検証すると、攻撃者がalg: noneに書き換えて、署名なしで通過させられます。検証コードはこちらが期待するアルゴリズムを固定する必要があります。

時計の問題も、実務で必ず出会います。exp/iatの検証には、60–120秒程度のclock skewの許容値を置きます。NTPがずれたサーバーが1台あるだけで断続的なログイン失敗が起きる障害は、原因の特定に長くかかります。

SAML 2.0: XMLであり、署名がすべて

SAMLは、XML文書1つ(Responseの中にAssertion)を、ブラウザーのPOSTで渡します。構造は次のようになっています。

<samlp:Response Destination="https://app.example.com/saml/acs"
                InResponseTo="_req123" IssueInstant="...">
  <saml:Issuer>https://idp.example.com/</saml:Issuer>
  <samlp:Status><samlp:StatusCode Value="...:Success"/></samlp:Status>
  <saml:Assertion>
    <saml:Issuer>https://idp.example.com/</saml:Issuer>
    <ds:Signature>...</ds:Signature>          ← 서명
    <saml:Subject>
      <saml:NameID Format="...emailAddress">hong@labhub.co.kr</saml:NameID>
      <saml:SubjectConfirmation>...</saml:SubjectConfirmation>
    </saml:Subject>
    <saml:Conditions NotBefore="..." NotOnOrAfter="...">
      <saml:AudienceRestriction>
        <saml:Audience>https://app.example.com/sp</saml:Audience>
      </saml:AudienceRestriction>
    </saml:Conditions>
    <saml:AttributeStatement>
      <saml:Attribute Name="department"><saml:AttributeValue>개발팀</saml:AttributeValue></saml:Attribute>
    </saml:AttributeStatement>
  </saml:Assertion>
</samlp:Response>

このコードブロックの韓国語は、2つとも、順に署名を指すコメントと、部署名の開発チームを表す属性値です。

SP(私たちのシステム)が検証すべきことは、OIDCと対応します。

  1. 署名: ResponseまたはAssertionに有効な署名があるか。 そして、署名された範囲の中に、信頼するデータが入っているか
  2. Issuer: 登録したIdPか
  3. Audience: 私たちのSPを対象に発行されたか
  4. NotBefore / NotOnOrAfter: 有効な時間か(clock skewを許容)
  5. InResponseTo: 送ったリクエストに対する応答か
  6. Destination: 私たちのACS URLに届いたものか
  7. 再利用の防止: 同じAssertion IDをすでに使ったことがあるか

項目1の2文目が、XML Signature Wrapping(XSW)攻撃の核心です。攻撃者は、元のAssertionを文書のどこかに隠しておき、署名されていない偽のAssertionを、パーサーが読む位置に入れます。署名の検証は通るのに、アプリが読むデータは偽物です。そのため、「署名が有効か」だけでなく、「自分が読んでいるそのノードが、署名されたノードか」を確認する必要があります。これが難しいので、SAMLは自前で実装せず、検証済みのライブラリを使うべきです。

SAML証明書の期限切れ: 静かに来る全面障害

SAMLでは、IdPの署名証明書を、SPがあらかじめ登録しておきます。その証明書が期限切れになると、すべてのユーザーのログインが同時に失敗します。実際に、大規模な移行プロジェクトで、署名証明書の期限切れにより47分間の全面ログイン障害が起きた事例があります。

防御は単純です。証明書の期限切れの監視一覧に、SAMLの署名証明書を必ず入れます。サーバーのTLS証明書だけを監視する一覧がほとんどなので、これが抜けます。

OIDCとSAML: 実務での選択

項目 SAML 2.0 OIDC
データ形式 XML JSON(JWT)
受け渡し経路 主にフロントチャネル(ブラウザーPOST) フロント+バックチャネル
モバイル/SPA 不向き 適合(PKCE)
メタデータの交換 XMLメタデータファイル discoveryドキュメント(JSON)
韓国の大企業/公共機関 今も多数 新規導入が増加
実装の難易度 高い(XML署名) 中程度

新規ならOIDC、相手がすでにSAMLならSAML。そして、面白い事実が1つあります。KeycloakのようなIdPにOIDCクライアントとSAMLクライアントを一緒に登録すると、同じSSOセッションで、一方はJWTを、もう一方はXMLのAssertionを受け取ります。IdPがプロトコル変換のハブの役割をするのです。レガシーと新規が共存するSI環境で、この性質がカットオーバー戦略の核心になります。