Authlib を利用して OpenID Connect (OIDC) フローを学ぶ

本記事では、以下の順で OIDC を理解し、実装できるように執筆しました。

  1. OIDC の仕様を紹介
  2. Authlib ライブラリを利用し、OIDC サーバーとクライアントを実装

なお、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 トークンの内容は以下のとおりです。

Final: OpenID Connect Core 1.0 incorporating errata set 2
OpenID Connect Core 1.0 incorporating errata set 2

OIDC と OAuth 2.0 の違い

「OpenID Connect (OIDC)」と「OAuth 2.0」の認証の違いは以下のとおりです。

https://en.wikipedia.org/wiki/OAuth
  • OpenID Connect (OIDC) は、アプリに証明書を渡し、Aさんを認証
  • OAuth 2.0 は、アプリに鍵を渡し、Aさんを認証

つまり、アプリに悪意があれば、鍵を使いたい放題使われるので、非常に危険というわけです。

スポンサーリンク

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 トークン」の関係は以下のとおりです。

https://qiita.com/TakahikoKawasaki/items/1c1bcf24b46ebd2030f5

JWS (JSON Web Signature)

JWS とは、JSON オブジェクトに署名メッセージ認証コード(MAC)を含めるために利用する仕様です。

JWS Serializations には次の2つのタイプが存在します。

JWS Compact Serialization

JWS Compact Serialization は、ピリオド (.)で区切られた以下の3つのメンバーを持つ URL-safe な文字列 (Base64 URL)です。

[ヘッダ or protected].[ペイロード].[署名]

JWS Compact Serialization でシリアライズ化した文字列の具体例は以下のとおりです。

eyJhbGciOiJIUzI1NiJ9.ZXhhbXBsZQ.VFVYg0kLRk7acZjnMT6NMwb39_IjXTTLqb8CzDEamj0

ヘッダの一覧は以下の RFC7515 に記載されてます。

RFC 7515: JSON Web Signature (JWS)
JSON Web Signature (JWS) represents content secured with digital signatures or Message Authentication Codes (MACs) using JSON-based data structures. Cryptograph...
JWS Compact Serialization にシリアライズ

以下の内容を JWS Compact Serialization にシリアライズ化します。

  • ヘッダ or protected:{"alg":"HS256"} ※非対称アルゴリズムを利用する場合は RS256 等を利用
  • ペイロード:example
  • 署名に利用する秘密の鍵の内容:secret ※ちゃんとした公開鍵、秘密鍵を生成する場合は後述記事を参照

署名に HS256 と RS256 のどちらを利用すればよいかについては以下のフォーラムをご覧ください。

https://answer-id.com/ja/64532583

秘密の鍵を "Secret" ではなく、実際の共通鍵、秘密鍵を生成して利用する場合は、以下の記事を参考にしてください。

■共通鍵の生成

■秘密鍵の生成

■シェルで JWS Compact Serialization にシリアライズ化する場合

echo -n '{"alg":"HS256"}'|base64
eyJhbGciOiJIUzI1NiJ9
echo -n 'example'|base64
ZXhhbXBsZQ==

なお、Base64 URL では、Base64 のパディング「==」を省略するので、「ZXhhbXBsZQ」となります。

echo -n 'eyJhbGciOiJIUzI1NiJ9.ZXhhbXBsZQ'|openssl dgst -binary -sha256 -hmac 'secret'|base64
VFVYg0kLRk7acZjnMT6NMwb39/IjXTTLqb8CzDEamj0=

Base64 URL では、/_ に置換する必要があるので、「VFVYg0kLRk7acZjnMT6NMwb39_IjXTTLqb8CzDEamj0

eyJhbGciOiJIUzI1NiJ9.ZXhhbXBsZQ.VFVYg0kLRk7acZjnMT6NMwb39_IjXTTLqb8CzDEamj0

■Python の Authlib ライブラリで JWS Compact Serialization にシリアライズ化する場合

上記のシェルで作成した JWS Compact Serialization と同じものを Python で作成します。

vim jws_compact.py
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)
python3 jws_compact.py
b'eyJhbGciOiJIUzI1NiJ9.ZXhhbXBsZQ.VFVYg0kLRk7acZjnMT6NMwb39_IjXTTLqb8CzDEamj0'
JWS Compact Serialization をデシリアライズ

■シェルで JWS Compact Serialization をデシリアライズする場合

echo eyJhbGciOiJIUzI1NiJ9|base64 -d
{"alg":"HS256"}
echo ZXhhbXBsZQ==|base64 -d
example

■Python の Authlib ライブラリで JWS Compact Serialization をデシリアライズする場合

vim jws_compact_deserialize.py
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)
python3 jws_compact_deserialize.py
シリアライズ: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

JWE Compact Serialization は、ピリオド (.)で区切られた以下の5つのメンバーを持つ URL-safe な文字列 (Base64 URL)です。

[ヘッダ].[暗号化されたキー].[初期ベクタ].[暗号文].[認証タグ]

JWE Compact Serialization にシリアライズ

以下の内容をシェルで JWS Compact Serialization にシリアライズ化します。

  • [ヘッダ or protected]:{'alg': 'RSA-OAEP', 'enc': 'A256GCM'}
  • [ペイロード]:example
openssl genrsa > private.key
openssl rsa -in private.key -pubout -out public.key

なお、秘密鍵、公開鍵については以下の記事をご覧ください。

vim jwe_compact.py
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)
python3 jwe_compact.py
b'eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.<暗号化されたキー>.OBRy7W45xRdE_Arn._f95nDWUDA.OnqJe-CJ_TT62VU_hApEZg'
JWE Compact Serialization をデシリアライズ
vim jwe_compact_deserialize.py
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)
python3 jwe_compact_deserialize.py
シリアライズした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
https://qiita.com/TakahikoKawasaki/items/1c1bcf24b46ebd2030f5

具体的には、以下のように JWS で「署名・MAC」したデータを暗号化して、JWE に含めます (オプション)。

https://logmi.jp/tech/articles/322822

OpenID Connect で利用する ID トークン (ユーザーに関する情報)は、この JWT を利用して「署名MAC暗号化(オプション)」します。

JWT エンコード

以下のデータを JWT にエンコードします。

  • ヘッダ or protected:{'alg':'RS256'}
  • ペイロード:{'hoge':'example'}
vim jwt.py
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)
python3 jwt.py
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 デコード

vim jwt_decode.py
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)
python3 jwt_decode.py
エンコード:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJob2dlIjoiZXhhbXBsZSJ9.<署名>

デコード:
{'hoge': 'example'}
スポンサーリンク

OpenID Connect プロバイダーの実装

ここからは、以下の github にあるコードを利用して、OpenID Connect プロバイダーを実装し、アクセストークンと ID トークンを取得します。

GitHub - authlib/example-oidc-server: Example for OpenID Connect 1.0 Server for Authlib.
Example for OpenID Connect 1.0 Server for Authlib. - GitHub - authlib/example-oidc-server: Example for OpenID Connect 1.0 Server for Authlib.
git clone https://github.com/authlib/example-oidc-server.git
cd example-oidc-server/
pip3 install -r requirements.txt
export AUTHLIB_INSECURE_TRANSPORT=1
flask initdb

※ エラーが発生する場合は、「website/routes.py」の「bp = Blueprint(__name__, 'home')」を以下のコードに修正してください。

bp = Blueprint('home', __name__)
flask run -h 0.0.0.0 -p 5000

Authorization Server に Client を登録

まずは http://127.0.0.1:5000/ にアクセスし、Resource Owner (ログインユーザー) を登録します。

今回は「test」ユーザーを作成し、[Login/Signup] をクリック

次に [Create Client] をクリックして、Client (アプリケーション) を登録します。

入力する情報は以下のとおりです。

Submit を押すと、クライアントの情報 (client_id や client_secret 等)が表示されます。

OpenID Connect プロバイダーでクライアントを認証

以下の手順で OpenID Connect プロバイダーで保護している REST API を実行します。

  1. Authorization Code GrantGrant Type を利用して、OpenID Connect プロバイダー (Authorization Server) からアクセストークンと ID トークンを取得
  2. アクセストークンと ID トークンで、Resource Server の /oauth/userinfo API を実行
https://docs.authlib.org/en/latest/oauth/2/intro.html#intro-oauth2

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 となります。

CODE=ABCDEFGHIJKLMNOPQRSTUVWXYZ

※ 自分のブラウザに表示された Authorization code に置き換えてください

curl -u "${CLIENT_ID}:${CLIENT_SECRET}" -XPOST http://127.0.0.1:5000/oauth/token -F grant_type=authorization_code -F code=${CODE}
{"access_token": "<アクセストークン>", "expires_in": 864000, "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.<ペイロード>.<署名>", "scope": "openid profile", "token_type"

ID トークンの確認

ID トークンの署名を検証し、正しい認可サーバーから発行されていることを確認できます。

また、取得した ID トークンのペイロードを確認できます。

echo ペイロード|base64 -d
{"iss":"https://authlib.org","aud":["xxx"],"iat":xxx,"exp":xxx,"auth_time":xxx,"nonce":"xxx","at_hash":"xxx","sub":"xxx","name":"xxx"}

ID トークンのクレーム (各フィールド) の意味は以下を参照してください。

Final: OpenID Connect Core 1.0 incorporating errata set 2
OpenID Connect Core 1.0 incorporating errata set 2

アクセストークンを利用して REST API を叩く

※${CLIENT_ID} と ${CLIENT_SECRET} は [Authorization Server に Client を登録]で取得したものに置き換えます。

curl -H "Authorization: Bearer ${access_token}" http://127.0.0.1:5000/oauth/userinfo
{"name":"test","sub":"1"}

※ ${access_token} は先程取得したアクセストークンに置き換えます。

関連記事

SSO (シングルサインオン) 入門記事の続きは以下のとおりです。


参考資料

How OpenID Connect Works - OpenID Foundation
What is OpenID Connect OpenID Connect is an interoperable authentication protocol based on the OAuth 2.0 framework of specifications (IETF RFC 6749 and 6750). I...
JOSE Guide
OAuth 2.0 / OIDC を理解する上で重要な3つの技術仕様
2020年3月17日、株式会社Authleteが主催する「OAuth & OIDC 勉強会 リターンズ【入門編】」が開催。同社の共同創業者であり、プログラマー兼代表取締役でもある川崎貴彦氏が、OAuth 2.0 / OIDCの仕様について解説しました。 冒頭は、OAuth 2.0の概念や認可・認証までの流れと、それを...
0