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

スポンサーリンク

OpenID Connect (OIDC) とは

OpenID Connect (OIDC) とは、認証の仕様です。
具体的には OAuth 2.0 に、ID トークン (ユーザーに関する情報) の発行を追加した仕様です。

(Identity, Authentication) + OAuth 2.0 = OpenID Connect

https://openid.net/connect/faq/

OpenID Connect を利用すると、OAuth 2.0認証に以下の機能を追加します。

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/

OpenID Connect では、以下で紹介するJOSE (JSON Object Signing and Encryption) という仕様を利用して ID トークンを「署名MAC暗号化」します。

なお、ID トークンの内容は以下のとおりです。

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

OAuth, OIDC について

暗号の基礎

ゼロトラストセキュリティ

OIDC と OAuth 2.0 の違い

「OpenID Connect (OIDC)」と「OAuth 2.0」の認証について、安全性の違いを示す図が wiki にあったので紹介します。

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

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

スポンサーリンク

JOSE (JSON Object Signing and Encryption)

JOSE とは、以下を利用して暗号化/署名/メッセージ認証コード(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) (RFC )
JWS Compact Serialization にシリアライズ

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

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

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

jwt - RS256とHS256の違いは何ですか? - Answer-ID
私はAuth0を使ってウェブアプリの認証を行っています。ASP.NET Core v1.0.0とAngular 2 rc5を使用していますが、一般的に認証やセキュリティについてはあまり詳しくありません。 ASP.NET Core Web jwt auth0 asp.net-core-webapi

秘密の鍵を "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 プロバイダーを利用して REST API を叩く

以下の手順で OpenID Connect で保護された REST API を叩きます。

  1. Authorization Code Grant を利用して、OpenID Connect プロバイダー (Authorization Server) からアクセストークンを取得
  2. 取得したアクセストークンで /oauth/userinfo API (Resource Server) を叩きます。
https://docs.authlib.org/en/latest/oauth/2/intro.html#intro-oauth2

OpenID Connect で利用する認可のフローは OAuth 2.0 のものです。

OAuth 2.0 の詳細については以下の記事をご覧ください。

また、REST API については以下の記事をご覧ください。

アクセストークンと ID トークンを取得

まずは、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 に置き換えます。

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"

アクセストークンを利用して 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} は先程取得したアクセストークンに置き換えます。

参考資料・おすすめの書籍

ゼロトラストセキュリティを学べる書籍

暗号系の知識を数式無しで習得できる書籍

OpenID Connect FAQ and Q&As | OpenID
Frequently Asked Questions (FAQs) and Question and Answer (Q&A) information for the OpenID Connect protocol.
JOSE Guide
The ultimate Python library in building OAuth and OpenID Connect servers. JWS, JWE, JWK, JWA, JWT are included.
OAuth 2.0 / OIDC を理解する上で重要な3つの技術仕様
2020年3月17日、株式会社Authleteが主催する「OAuth & OIDC 勉強会 リターンズ【入門編】」が開催。同社の共同創業者であり、プログラマー兼代表取締役でもある川崎貴彦氏が、OAuth 2.0 / OIDCの仕様について解説しました。 冒頭は、OAuth 2.0の概念や認可・認証までの流れと、それを...