本記事では、以下の順で OIDC を理解し、実装できるように執筆しました。
- OIDC の仕様を紹介
- Authlib ライブラリを利用し、OIDC サーバーとクライアントを実装
なお、SSO について学習する場合は、以下の順で学習することをおすすめします。
- 【SSO (シングルサインオン) 入門1】ID フェデレーションとは
- 【SSO (シングルサインオン) 入門2】OAuth 2.0 とは
- 【SSO (シングルサインオン) 入門3】OpenID Connect (OIDC) とは ←イマココ
- 【SSO (シングルサインオン) 入門4】Keycloak で SSO を実装
OpenID Connect (OIDC) とは
OpenID Connect (OIDC) とは、OAuth 2.0 に認証および ID トークン (ユーザーに関する情報) を発行する仕様を追加したものです。
(Identity, Authentication) + OAuth 2.0 = OpenID Connect
https://openid.net/connect/faq/
そのため、OAuth 2.0 を知らない方は、最初に以下の記事を読むことをおすすめします。
OIDC では、OAuth 2.0 の認証に「署名・MAC・暗号化」機能を持つ ID トークンを追加します。
OIDC の機能 | 役割 |
---|---|
署名 | 認証 (ID トークンを発行する相手が正しいことを確認) |
メッセージ認証コード(MAC) | 完全性 (ID トークンが欠損・改ざんしてないことを確認) |
暗号化 (オプション) | 機密性 (ID トークンの盗聴を防ぐ) |
OAuth 2.0, the substrate for OpenID Connect, outsources the necessary encryption to the Web’s built-in TLS (also called HTTPS or SSL) infrastructure,
OpenID Connect uses standard JSON Web Token (JWT) data structures when signatures are required.This makes OpenID Connect dramatically easier for developers to implement, and in practice has resulted in much better interoperability.
https://openid.net/connect/faq/
なお、ID トークンの内容は以下のとおりです。
OIDC と OAuth 2.0 の違い
「OpenID Connect (OIDC)」と「OAuth 2.0」の認証の違いは以下のとおりです。
つまり、アプリに悪意があれば、鍵を使いたい放題使われるので、非常に危険というわけです。
JOSE (JSON Object Signing and Encryption)
JOSE とは、以下を利用して ID トークンを 暗号化/署名/メッセージ認証コード(MAC)をする仕様です。
・JWS (JSON Web Signature):署名/メッセージ認証コード(MAC)
・JWE (JSON Web Encryption):暗号化
・JWK (JSON Web Key):暗号化キー
・JWA (JSON Web Algorithms):暗号化アルゴリズム
The JSON Object Signing and Encryption (JOSE) technologies -- JSON Web Signature [JWS], JSON Web Encryption [JWE], JSON Web Key [JWK], and JSON Web Algorithms [JWA] -- can be used collectively to encrypt and/or sign content using a variety of algorithms.
https://datatracker.ietf.org/doc/html/rfc7520
「JOSE」と「ID トークン」の関係は以下のとおりです。
JWS (JSON Web Signature)
JWS とは、JSON オブジェクトに署名・メッセージ認証コード(MAC)を含めるために利用する仕様です。
JWS Serializations には次の2つのタイプが存在します。
- JWS Compact Serialization (OIDC のID トークンで使うのはこちら)
- JWS JSON Serialization
JWS Compact Serialization
JWS Compact Serialization は、ピリオド (.)で区切られた以下の3つのメンバーを持つ URL-safe な文字列 (Base64 URL)です。
[ヘッダ or protected].[ペイロード].[署名]
JWS Compact Serialization でシリアライズ化した文字列の具体例は以下のとおりです。
eyJhbGciOiJIUzI1NiJ9.ZXhhbXBsZQ.VFVYg0kLRk7acZjnMT6NMwb39_IjXTTLqb8CzDEamj0
ヘッダの一覧は以下の RFC7515 に記載されてます。
JWS Compact Serialization にシリアライズ
以下の内容を JWS Compact Serialization にシリアライズ化します。
- ヘッダ or protected:{"alg":"HS256"} ※非対称アルゴリズムを利用する場合は RS256 等を利用
- ペイロード:example
- 署名に利用する秘密の鍵の内容:secret ※ちゃんとした公開鍵、秘密鍵を生成する場合は後述記事を参照
署名に HS256 と RS256 のどちらを利用すればよいかについては以下のフォーラムをご覧ください。
秘密の鍵を "Secret" ではなく、実際の共通鍵、秘密鍵を生成して利用する場合は、以下の記事を参考にしてください。
■共通鍵の生成
■秘密鍵の生成
■シェルで JWS Compact Serialization にシリアライズ化する場合
eyJhbGciOiJIUzI1NiJ9
ZXhhbXBsZQ==
なお、Base64 URL では、Base64 のパディング「==」を省略するので、「ZXhhbXBsZQ」となります。
VFVYg0kLRk7acZjnMT6NMwb39/IjXTTLqb8CzDEamj0=
Base64 URL では、/
を _
に置換する必要があるので、「VFVYg0kLRk7acZjnMT6NMwb39_IjXTTLqb8CzDEamj0」
■Python の Authlib ライブラリで JWS Compact Serialization にシリアライズ化する場合
上記のシェルで作成した JWS Compact Serialization と同じものを Python で作成します。
from authlib.jose import JsonWebSignature jws = JsonWebSignature(algorithms=['HS256']) #JWS で利用するアルゴリズム #シリアライズ化するデータ protected = {'alg': 'HS256'} payload = b'example' secret='secret' compact_serialization = jws.serialize_compact(protected, payload, secret) print(compact_serialization)
b'eyJhbGciOiJIUzI1NiJ9.ZXhhbXBsZQ.VFVYg0kLRk7acZjnMT6NMwb39_IjXTTLqb8CzDEamj0'
JWS Compact Serialization をデシリアライズ
■シェルで JWS Compact Serialization をデシリアライズする場合
{"alg":"HS256"}
example
■Python の Authlib ライブラリで JWS Compact Serialization をデシリアライズする場合
from authlib.jose import JsonWebSignature jws = JsonWebSignature(algorithms=['HS256']) protected = {'alg': 'HS256'} payload = b'example' secret='secret' compact_serialization = jws.serialize_compact(protected, payload, secret) print("シリアライズ:", end="") print(compact_serialization) key ='secret' data = jws.deserialize_compact(compact_serialization, key) print("デシリアライズ:", end="") print(data)
シリアライズ:b'eyJhbGciOiJIUzI1NiJ9.ZXhhbXBsZQ.VFVYg0kLRk7acZjnMT6NMwb39_IjXTTLqb8CzDEamj0' デシリアライズ:{'header': {'alg': 'HS256'}, 'payload': b'example'}
JWS JSON Serialization
JWS JSON Serialization は次の2つの構文を持つタイプです。
・General JWS JSON Serialization 構文
・Flattened JWS JSON Serialization 構文
General JWS Compact Serialization は以下のフォーマットで、複数の署名 or MAC に利用します。
{ "payload":"<ペイロードの中身>", "signatures":[ { "protected":"<protected の中身>", "header":{<ヘッダの中身>}, "signature":"<署名の中身>" }, { "protected":"<protected の中身>", "header":{<ヘッダの中身>}, "signature":"<署名の中身>" } ] }
Flattened JWS JSON Serialization は以下のフォーマットで、単一の署名 or MAC に利用します。
{ "payload":"<ペイロードの中身>", "protected":"<protected の中身>", "header":{"<ヘッダの中身>"}, "signature":"<署名の中身>" }
JWE (JSON Web Encryption)
JWE とは、JSON オブジェクトを暗号化するために利用する仕様です。
JWE Serializations には次の2つのタイプが存在します。
- JWE Compact Serialization (OIDC のID トークンで使うのはこちら)
- JWE JSON Serialization (authlib ライブラリでは現時点で実装されていないので割愛します)
JWE Compact Serialization
JWE Compact Serialization は、ピリオド (.)で区切られた以下の5つのメンバーを持つ URL-safe な文字列 (Base64 URL)です。
[ヘッダ].[暗号化されたキー].[初期ベクタ].[暗号文].[認証タグ]
JWE Compact Serialization にシリアライズ
以下の内容をシェルで JWS Compact Serialization にシリアライズ化します。
- [ヘッダ or protected]:{'alg': 'RSA-OAEP', 'enc': 'A256GCM'}
- [ペイロード]:example
なお、秘密鍵、公開鍵については以下の記事をご覧ください。
from authlib.jose import JsonWebEncryption jwe = JsonWebEncryption(algorithms=['RSA-OAEP','A256GCM']) #JWE で利用するアルゴリズム #シリアライズ化するデータ protected = {'alg': 'RSA-OAEP', 'enc': 'A256GCM'} payload = b'example' with open('public.key', 'rb') as f: #暗号化に利用する公開鍵 key = f.read() compact_serialization = jwe.serialize_compact(protected, payload, key) print(compact_serialization)
b'eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.<暗号化されたキー>.OBRy7W45xRdE_Arn._f95nDWUDA.OnqJe-CJ_TT62VU_hApEZg'
JWE Compact Serialization をデシリアライズ
from authlib.jose import JsonWebEncryption jwe = JsonWebEncryption(algorithms=['RSA-OAEP','A256GCM']) #JWE で利用するアルゴリズム #シリアライズ化するデータ protected = {'alg': 'RSA-OAEP', 'enc': 'A256GCM'} payload = b'example' with open('public.key', 'rb') as f: #暗号化に利用する公開鍵 key = f.read() compact_serialization = jwe.serialize_compact(protected, payload, key) print("シリアライズしたJWE") print(compact_serialization) with open('private.key', 'rb') as f: #復号化に利用する秘密鍵 key = f.read() data = jwe.deserialize_compact(compact_serialization, key) print("\nデシリアライズしたJWE") print(data)
シリアライズしたJWE b'eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.<暗号化されたキー>.RLiUs4fV-SSj_jzG.nfdbRVIMjQ.bvrSO0HOEZdtxRP9lTl-zw' デシリアライズしたJWE {'header': {'alg': 'RSA-OAEP', 'enc': 'A256GCM'}, 'payload': b'example'}
JWT (JSON Web Token)
JWT は JSON を 「JWS で署名、MAC」したものを、「JWE で暗号化」したものです。
JSON Web Token (JWT)
A string representing a set of claims as a JSON object that is encoded in a JWS or JWE, enabling the claims to be digitally signed or MACed and/or encrypted.
https://datatracker.ietf.org/doc/html/rfc7519
具体的には、以下のように JWS で「署名・MAC」したデータを暗号化して、JWE に含めます (オプション)。
OpenID Connect で利用する ID トークン (ユーザーに関する情報)は、この JWT を利用して「署名・MAC・暗号化(オプション)」します。
JWT エンコード
以下のデータを JWT にエンコードします。
- ヘッダ or protected:{'alg':'RS256'}
- ペイロード:{'hoge':'example'}
from authlib.jose import jwt #エンコードするデータ header = {'alg':'RS256'} payload = {'hoge':'example'} #署名する秘密鍵 with open('private.key', 'rb') as f: key = f.read() serialization = jwt.encode(header, payload, key) print(serialization)
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJob2dlIjoiZXhhbXBsZSJ9.<署名>
※ 上記の JWT は JWS でシリアライズしただけなので、Nasted JWT を作成する場合は、JWE で暗号化する必要があります。
JWT payload with JWS is not encrypted, it is just signed.
You can also use JWT with JWE which is encrypted.
https://docs.authlib.org/en/v0.15.3/jose/jwt.html
JWT デコード
from authlib.jose import jwt #エンコードするデータ header = {'alg':'RS256'} payload = {'hoge':'example'} #エンコードで署名する秘密鍵 with open('private.key', 'rb') as f: key = f.read() serialization = jwt.encode(header, payload, key) print("エンコード:") print(serialization) #デコードするための公開鍵 with open('public.key', 'rb') as f: key = f.read() claims = jwt.decode(serialization, key) print("\nデコード:") print(claims)
エンコード: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJob2dlIjoiZXhhbXBsZSJ9.<署名> デコード: {'hoge': 'example'}
OpenID Connect プロバイダーの実装
ここからは、以下の github にあるコードを利用して、OpenID Connect プロバイダーを実装し、アクセストークンと ID トークンを取得します。
※ エラーが発生する場合は、「website/routes.py」の「bp = Blueprint(__name__, 'home')」を以下のコードに修正してください。
bp = Blueprint('home', __name__)
Authorization Server に Client を登録
まずは http://127.0.0.1:5000/ にアクセスし、Resource Owner (ログインユーザー) を登録します。
次に [Create Client] をクリックして、Client (アプリケーション) を登録します。
入力する情報は以下のとおりです。
Submit を押すと、クライアントの情報 (client_id や client_secret 等)が表示されます。
OpenID Connect プロバイダーでクライアントを認証
以下の手順で OpenID Connect プロバイダーで保護している REST API を実行します。
- Authorization Code Grant の Grant Type を利用して、OpenID Connect プロバイダー (Authorization Server) からアクセストークンと ID トークンを取得
- アクセストークンと ID トークンで、Resource Server の /oauth/userinfo API を実行
OpenID Connect で利用する認可のフローは OAuth 2.0 のものです。
アクセストークンと ID トークンを取得
1.ブラウザから認可サーバーにアクセス (scope は openid)
http://127.0.0.1:5000/oauth/authorize?client_id=${CLIENT_ID}&scope=openid+profile&response_type=code&nonce=abc
※${CLIENT_ID} は [Authorization Server に Client を登録]で取得した client_id に置き換えます。
2. Consent にチェックを入れ、Submit をクリックする。
上記の赤線を引いた文字列が Authorization code となります。
※ 自分のブラウザに表示された Authorization code に置き換えてください
{"access_token": "<アクセストークン>", "expires_in": 864000, "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.<ペイロード>.<署名>", "scope": "openid profile", "token_type"
ID トークンの確認
ID トークンの署名を検証し、正しい認可サーバーから発行されていることを確認できます。
また、取得した ID トークンのペイロードを確認できます。
{"iss":"https://authlib.org","aud":["xxx"],"iat":xxx,"exp":xxx,"auth_time":xxx,"nonce":"xxx","at_hash":"xxx","sub":"xxx","name":"xxx"}
ID トークンのクレーム (各フィールド) の意味は以下を参照してください。
アクセストークンを利用して REST API を叩く
※${CLIENT_ID} と ${CLIENT_SECRET} は [Authorization Server に Client を登録]で取得したものに置き換えます。
{"name":"test","sub":"1"}
※ ${access_token} は先程取得したアクセストークンに置き換えます。
関連記事
SSO (シングルサインオン) 入門記事の続きは以下のとおりです。
- 【SSO (シングルサインオン) 入門1】ID フェデレーションとは
- 【SSO (シングルサインオン) 入門2】OAuth 2.0 とは
- 【SSO (シングルサインオン) 入門3】OpenID Connect (OIDC) とは ←イマココ
- 【SSO (シングルサインオン) 入門4】Keycloak で SSO を実装