前提
- Open ID ConnectのRelying Party側(以降RP)のフローをやってみる教材はAuth屋さんの本などがあるが、RP側の挙動をやってみるだけではまだまだOIDCの理解が深まっていないと感じる。
- そこで年末年始ということもあり、DjangoでOIDCのIdP側をブログを書きながら実装してみることにした
※ Djangoを使うのは以下の理由からで、深い意味はない
Configurationエンドポイント
まずはディスカバリというIdPのエンドポイント一覧を定義するURLを実装する。
https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest
.well-known/openid-configuration
というパスがよく使われる印象なので、本実装でもそのようにする。
Viewを実装する
まだIdPのエンドポイントを何も実装していないので、空のJSONを返すViewを実装する。
views.py
from django.http import JsonResponse from django.views import View class DiscoveryView(View): def get(self, request): return JsonResponse({})
url.pyに/.well-known/openid-configuration
を定義する
urls.py
from django.urls import path from . import views app_name = "sampleapp" urlpatterns = [ path("`.well-known/openid-configuration/`", views.DiscoveryView.as_view(), name="discovery"), ]
これで.well-known/openid-configuration
にアクセスすると空のJSONが返ってくるようになった。
$ curl -X GET "http://localhost:8000/sample/.well-known/openid-configuration/" {}
認可エンドポイント
認可エンドポイントを実装していく
https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
エンドポイントの追加
まずは先ほどのディスカバリに認可エンドポイントのURLを定義する。
views.py
class DiscoveryView(View): def get(self, request): return JsonResponse({ "authorization_endpoint": "http://localhost:8000/sample/authorize/", })
これでディスカバリからエンドポイントを調べられるようになる。
$ curl -X GET "http://localhost:8000/sample/.well-known/openid-configuration/" {"authorization_endpoint": "http://localhost:8000/sample/authorize/"}
Formの実装
Formにリクエストパラメータを定義していく。とりあえず以下を定義する。PKCE
を実装する場合はcode_challenge
とcode_challenge_method
が必要だが、最初はそこまではやらない。
パラメータ名 | 説明 |
---|---|
response_type | フローの種類を定義する。今回は認可コードフローなので固定でcode という文字列を渡す |
scope | 要求するユーザー情報を半角スペースで区切って渡す。(例 email profile ) |
client_id | RPに発行したクライアントID |
state | リクエスト時と状態をコールバック時に渡すために使うらしい。CSRF対策にも使える。※ セキュリティ対策の文脈で解説されることの方が多い印象 |
redirect_uri | コールバック時のRPへの戻り先URL |
nonce | セキュリティ対策用のパラメータらしい。ランダム文字列でよい。詳しくは必要になったら理解する。 |
とりあえずこれらのパラメータをFormに定義する
forms.py
class AuthorizeForm(forms.Form): response_type = forms.CharField() scope = forms.CharField() client_id = forms.CharField() state = forms.CharField() redirect_uri = forms.CharField() nonce = forms.CharField()
response_type
はcode
固定なのでバリデーションする
def clean_response_type(self): response_type = self.cleaned_data["response_type"] if response_type != "code": raise forms.ValidationError("Invalid response_type") return response_type
scope
は半角スペース区切りの文字列の仕様だが、プログラムの中ではリストで扱いたいので変換する。
def clean_scope(self): return self.cleaned_data["scope"].split()
OIDCの仕様でopenid
という文字列は必ず含む必要があるのでバリデーションする
def clean_scope(self): scope = self.cleaned_data["scope"].split() if "openid" not in scope: raise forms.ValidationError("Invalid scope") return scope
Viewの実装
いったんパラメータを受け取ってprintデバッグでパラメータを出力するまでとする。
views.py
class AuthorizeView(View): def get(self, request): form = AuthorizeForm(request.GET) if not form.is_valid(): return HttpResponseBadRequest() print("authorize params: ", form.cleaned_data) return HttpResponse("todo")
url.pyに/authorize/
を定義する
urls.py
path("authorize/", views.AuthorizeView.as_view(), name="authorize"),
動作確認
以下のcurlを実行してみる。scope
の半角スペースは%20
に置換しないとcurl: (3) URL rejected: Malformed input to a URL function
が発生するので注意する。
curl -X GET "http://localhost:8000/sample/authorize/?response_type=code&scope=openid%20email&client_id=test&state=abcd&redirect_uri=http://localhost/callback&nonce=efgh"
以下のprintデバッグ結果が出力できた。
authorize params: {'response_type': 'code', 'scope': ['openid', 'email'], 'client_id': 'test', 'state': 'abcd', 'redirect_uri': 'http://localhost/callback', 'nonce': 'efgh'}`
今日はここまでとする。
参考にしているもの
Auth屋さんの本
各エンドポイントの仕様はAuth屋さんの本を authya.booth.pm
M3さんのテックブログの「フルスクラッチして理解するOpenID Connect 」シリーズ
実装する上で参考にさせて頂いています。