NetworkTips

Entra IDのSSOをトークンの中身から理解する【認証と認可・SAMLとOIDCの違い】

スポンサーラベル
IDトークンとアクセストークンの中身の違い。認証はoidとname、認可はscope Network

当サイトはアフィリエイト広告を利用しています。

Entra ID(旧 Azure AD)で社内システムにSSOを入れる話になると、認証、認可、IdP、SP、アサーション、トークン、SAML、OIDCと用語が一度に出てきます。設定画面を触る前に、何がどう渡っているのかを掴んでおきたいところです。

この記事では、実際にトークンを組み立てて中身を開くところから始めます。認証と認可の違いは、言葉で覚えるより何が入っているかを見るほうが早いからです。

IDトークンとアクセストークンの中身の違い。認証はoidとname、認可はscope

後半では、Entra IDで実際に詰まる3点(sub と oid の違い、認可に使えないクレーム、グループが消える条件)と、SAMLとOIDCの選び分けを扱います。

エンドポイントとクレームの仕様は、Entra IDが公開しているディスカバリ文書と公式リファレンスで確認しています。 トークンは手元で組み立てたもので、実際のテナントとの接続試験は行っていません。

1. 認証と認可は別のもの

まず言葉の整理です。

問い英語
認証あなたは誰かAuthentication(AuthN)
認可あなたは何をしてよいかAuthorization(AuthZ)

社屋への入館で例えると、受付で社員証を見せて本人だと確認されるのが認証、その社員証でサーバー室の扉が開くかどうかが認可です。本人だと分かっていても、権限が無ければ扉は開きません。

Entra IDでは、認証の結果がIDトークン、認可の結果がアクセストークンとして発行されます。どちらもJWTです。

2. トークンを組み立てて中身を開く

JWTはピリオドで区切られた3つの文字列です。ライブラリが無くても作れます。

import base64, hashlib, hmac, json

def b64u(d):
    # JWTはURL安全なBase64で、末尾の = を落とす
    return base64.urlsafe_b64encode(d).rstrip(b"=").decode()

def make_jwt(payload, secret):
    header = {"typ": "JWT", "alg": "RS256", "kid": "kWbkaa6qs8wsTnBwiiNYOhHbnAw"}
    seg = (b64u(json.dumps(header, separators=(",", ":")).encode()) + "."
           + b64u(json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode()))
    sig = b64u(hmac.new(secret, seg.encode(), hashlib.sha256).digest())
    return seg + "." + sig

2.1 IDトークン(認証の結果)

Entra ID の v2.0 エンドポイントが発行するIDトークンは、おおむね次の形です。

{
  "aud": "11112222-3333-4444-5555-666677778888",
  "iss": "https://login.microsoftonline.com/72f988bf-.../v2.0",
  "iat": 1790000000,
  "nbf": 1790000000,
  "exp": 1790003600,
  "name": "情シス 太郎",
  "oid": "a1b2c3d4-1111-2222-3333-556677889900",
  "preferred_username": "(UPN。メール形式の文字列)",
  "sub": "ZxQ9k0q8Z3mN7pL2vXyT1cDfGhJkLmNoPqRsTuVw",
  "tid": "72f988bf-0000-0000-0000-2d7cd011db47",
  "ver": "2.0",
  "roles": ["Timecard.Approver"]
}

aud はアプリ(アプリケーションID)、tid はテナントID、oid は利用者のオブジェクトIDです。「誰が」「どのテナントの」「どのアプリに」サインインしたかが入っています。

ヘッダーはこうなります。

{"typ":"JWT","alg":"RS256","kid":"kWbkaa6qs8wsTnBwiiNYOhHbnAw"}

kid は署名に使った鍵の識別子です。アプリはこの値を手がかりに、Entra IDの公開鍵置き場から正しい鍵を選んで検証します。Entra IDのIDトークンの署名アルゴリズムは RS256 のみです(ディスカバリ文書の id_token_signing_alg_values_supported で確認できます)。

2.2 アクセストークン(認可の結果)

同じサインインで発行されるもう1枚です。

{
  "aud": "https://graph.microsoft.com",
  "iss": "https://login.microsoftonline.com/72f988bf-.../v2.0",
  "exp": 1790000900,
  "scp": "User.Read Mail.Read",
  "oid": "a1b2c3d4-1111-2222-3333-556677889900",
  "azp": "11112222-3333-4444-5555-666677778888"
}

scp(スコープ)が入りました。 aud もアプリではなくAPI側を指しています。このトークンは「誰であるか」をアプリに伝えるためのものではなく、「このAPIのこの操作をしてよい」とAPIに伝えるためのものです。

クライアントを表す値も名前が変わります。v2.0では azp、v1.0では appid です。移行時に参照している側が壊れる箇所なので覚えておいてください。

なお name や preferred_username はアクセストークンにも入り得ます(profile スコープを要求した場合)。入っているかどうかではなく、何のために発行されたトークンかで使い分けるのが正しい見方です。

有効期限は、アクセストークンのほうが短いとは限りません。 公式によると、アクセストークンの既定は60〜90分のランダム値(平均75分)で、クライアントやリソース、条件付きアクセスの有無で変わります。継続的アクセス評価(CAE)に対応していると24〜28時間まで延びることもあります。一方 IDトークンとSAMLトークンの既定は1時間で固定です。

3. トークンは暗号化されていない

ここを誤解したまま設計すると事故ります。

# 署名を検証せずに、2番目のセグメントをデコードするだけ
payload = id_token.split(".")[1]
print(json.loads(base64.urlsafe_b64decode(payload + "=" * (-len(payload) % 4))))

鍵が無くても中身は読めます。 Base64はエンコードであって暗号ではありません。

3.1 では署名は何を守っているのか

改ざんの検知です。中身を書き換えて署名はそのまま使い回すと、こうなります。

元のトークンの検証        : True
書き換えた後の検証        : False
書き換えた中身は読める    : admin

読めるが、書き換えるとバレる。 だからアプリ側は必ず署名を検証してから中身を使う必要があります。検証を省いてデコードだけして使うと、誰でも管理者になれてしまいます。

4. Entra IDのクレームで詰まる3点

ここからがEntra ID固有の話です。公式リファレンスに明記されているのに、見落とされがちな点を3つ挙げます。

4.1 sub はアプリごとに変わる。横断するなら oid

一番の落とし穴です。sub はアプリ(クライアントID)ごとに違う値になります。

アプリA の sub : ZxQ9k0q8Z3mN7pL2vXyT1cDfGhJkLmNoPqRsTuVw
アプリB の sub : 8vHt2LqWxY4nR6sK9pZmC3bVfDgJhNkOuTrEwQaI   ← 別の値
両方の oid     : a1b2c3d4-1111-2222-3333-556677889900       ← 同じ

公式リファレンスにも、sub は「ペアワイズ識別子であり、アプリケーションIDごとに一意」と書かれています。一方 oid は「2つの異なるアプリが同じ利用者をサインインさせると、同じ値を受け取る」とされています。

つまり、複数のシステムで同じ人を突き合わせたいなら oid を使います。 sub を主キーにして2つ目のアプリを繋いだ時点で、同じ人が別人として登録されます。

ただしテナントをまたぐと oid も変わります。 ゲストユーザーは別アカウント扱いになるため、テナント横断での名寄せには使えません。

4.2 preferred_username と email は認可に使えない

どちらも可変だからです。公式リファレンスは preferred_username について「値は可変で、時間とともに変わる可能性がある。可変であるため、この値を認可の判断に使うことはできない」と明記しています。

email についても「正確である保証はなく、時間とともに変わる。認可やユーザーのデータ保存には決して使わないこと」とあります。

姓が変わってUPNが変わる、退職者のアドレスが別人に再割り当てされる、といったことは実際に起きます。画面に出す名前として使うのはよいが、権限判定とデータの紐付けには oid を使う、と覚えておけば間違いません。

name も「一意である保証はなく、変更され得る。表示目的にのみ使うこと」とされています。

4.3 グループが多いと groups クレームが消える

Entra IDはグループ情報をトークンに入れられますが、上限があります。

トークン形式上限
SAML150グループ
JWT(OIDC)200グループ

これを超えると、groups クレームは入りません。代わりにこうなります。

{
  "_claim_names": { "groups": "src1" },
  "_claim_sources": {
    "src1": {
      "endpoint": "https://graph.microsoft.com/v1.0/users/{oid}/getMemberObjects"
    }
  }
}

アプリ側がMicrosoft Graphを叩いてグループを取り直す必要があります。

厄介なのは、検証環境では動いて本番で動かないという形で出ることです。検証用アカウントの所属グループは少なく、本番の利用者は何十、何百のグループに入っているためです。

グループで権限を判定する設計なら、最初からオーバーフロー時の処理を実装しておくか、groups ではなくアプリロール(roles)で判定するのが安全です。アプリロールはそのアプリに割り当てたものだけが入るので、数が膨らみません。

5. SAMLとOIDCの違い

Entra IDはどちらも使えます。エンタープライズアプリケーションの登録時に選ぶ形です。

SAML 2.0OIDC
策定2005年2014年
土台XML SignatureOAuth 2.0 + JWT
形式XMLJSON
認証結果アサーションIDトークン
アプリ側の呼び名SP(Service Provider)RP(Relying Party)
Entra IDでの登録エンタープライズアプリケーション(SAML)アプリの登録(OIDC)

呼び名が違うだけで対応する概念が多いので、対照しておくと読み替えが楽になります。

5.1 ログインの流れを並べる

どこと通信するかを図にします。まずSAMLです。

SAMLのSP-initiatedフロー。利用者のブラウザを経由して、業務アプリとIdPの間でAuthnRequestとアサーションがやり取りされる

SAMLでは、アプリとEntra IDが直接通信しません。 すべて利用者のブラウザを経由します。だからアプリからEntra IDへネットワークが到達できなくても成立します。アプリが社内の閉じた場所にあっても繋がるのはこのためです。

次にOIDCです。

OIDCの認可コードフロー。認可コードを受け取った後、業務アプリとOpenID Providerがサーバー間で直接トークンをやり取りする

最後の2往復(8と9)がサーバー間通信になっているのが決定的な違いです。ブラウザには認可コードという引換券しか渡さず、中身のあるトークンはサーバー同士で受け渡すため、トークンがブラウザの履歴やログに残りません。

その代わり、アプリから login.microsoftonline.com へHTTPSで到達できる必要があります。閉域に置いたアプリでは、ここで詰まることがあります。プロキシ経由にするなら、その設定も要ります。

5.2 サイズを実測する

同じ内容を伝えるのに、どれだけ差があるかを測りました。SAMLは署名値と証明書を省略した状態です。

SAML(XMLそのもの)        :   2661 バイト
SAML(Base64化・POST送信)  :   3548 バイト
SAML(deflate+Base64・GET) :   1192 バイト
OIDC(IDトークン JWT)      :    355 バイト

POST送信するSAMLは、JWTのおよそ10倍です。しかもこれは署名値と証明書を省いた数字で、実物はさらに2〜3KB増えます。

この大きさが、SAMLがURLではなくPOSTで送られる理由です。SAMLのログインで一瞬だけ白い画面が出て自動的に遷移するのは、裏で自動送信フォームがPOSTしているからです。

5.3 v1.0とv2.0でissuerのホストが違う

Entra IDには2世代のエンドポイントがあり、発行者の値が別物です。ディスカバリ文書で確認できます。

v2.0 の issuer : https://login.microsoftonline.com/{tenantid}/v2.0
v1.0 の issuer : https://sts.windows.net/{tenantid}/

ドメインごと違います。アプリ側でissuerを検証していると、世代を変えた瞬間に落ちます。移行時はここを必ず確認してください。

クレーム名も違い、preferred_username は v2.0 のみ、unique_name は v1.0 のみです。新規に作るなら v2.0 を選びます。

設定値は推測せず、テナントのディスカバリ文書を見るのが確実です。

https://login.microsoftonline.com/{テナントID}/v2.0/.well-known/openid-configuration

認可エンドポイント、トークンエンドポイント、公開鍵の置き場所(jwks_uri)、ログアウトURLが全部載っています。なお UserInfo エンドポイントだけは Microsoft Graph 側(https://graph.microsoft.com/oidc/userinfo)にあります。

6. どちらを選ぶか

  • アプリが対応している方を選ぶ。これが9割です
  • 両方対応しているなら OIDC。軽く、モバイルやAPIにも伸ばせます
  • SAMLしか対応していない業務パッケージは今も多い。古いという理由だけで避ける必要はありません
  • アプリからEntra IDへ出られないなら SAML。ブラウザ経由で完結します

どちらを選んでもEntra ID側で管理するものは同じです。利用者、グループ、条件付きアクセス。プロトコルはその出力形式の違いにすぎません。

7. よくある誤解

「SSOを入れればパスワードが無くなる」 無くなりません。Entra IDへのサインインは残ります。入力する回数と場所が1か所に集約されるだけです。そのぶんEntra ID側の守りが重要になります。

「OIDCはOAuth 2.0と同じ」 違います。OAuth 2.0は認可の仕組みで、本来「誰か」を伝える機能はありません。そこに認証の層を足したのがOIDCです。OAuth 2.0を認証に流用すると、アクセストークンを持っている=本人、という誤った判定になります。

「JWTは安全だから中身を見られても平気」 3章のとおり中身は読めます。安全なのは改ざんされないことであって、秘密が守られることではありません。

「アクセストークンでユーザーを判定してよい」 アクセストークンはAPI向けで、アプリが中身を読むことを前提にしていません。利用者が誰かを知りたいならIDトークンを使います。

8. まとめ

Entra IDのSSOについて、トークンの中身から整理しました。

  • 認証は「誰か」、認可は「何をしてよいか」。 アクセストークンにだけ scp が入る。入っているクレームではなく、何のために発行されたかで使い分ける
  • アクセストークンのほうが短命とは限らない。 既定は60〜90分のランダムで、CAE対応なら24〜28時間まで延びる。IDトークンは1時間
  • クライアントを表すクレームは v2.0 が azp、v1.0 が appid
  • JWTはピリオド区切りのBase64が3つ。 中身は読めるが、書き換えると署名が合わなくなる
  • sub はアプリごとに変わる。 複数システムで名寄せするなら oid を使う
  • preferred_username と email は可変。 認可判断とデータ紐付けに使ってはいけない
  • グループは SAML 150/JWT 200 を超えると消える。 検証環境で気づけないので、アプリロールで判定するほうが安全
  • v1.0とv2.0でissuerのホストが違う。 移行時に検証で落ちる
  • SAMLはブラウザ経由で完結、OIDCはサーバー間通信が要る

設定値はディスカバリ文書を見れば確定します。推測で埋めずに、テナントの実際の値を確認してください。

Entra IDとアプリに実際に何を登録するかはEntra IDのSSO設定でEntra側とアプリ側に何を登録するかにまとめています。

OIDCで実際にWebサーバーを保護する例はApacheでADFSによるOpenIDConnect認証を試してみるにあります(IdPはADFSですが、プロトコルの流れは同じです)。LDAPによる認証連携はCentOS7でPostfix+dovecotをAD連携して構築、Linux側のAD参加はLinuxサーバのActiveDirectoryへの参加手順にまとめています。

コメント

タイトルとURLをコピーしました