以下の続き kamatimaru.hatenablog.com
トークンエンドポイント
トークンエンドポイント(=認可コードを受け取ってトークンを発行するエンドポイント)を実装していく。
https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
ディスカバリへの登録
最初にディスカバリにトークンエンドポイントのURLを追加する
class DiscoveryView(View): def get(self, request): return JsonResponse( { "authorization_endpoint": "http://localhost:8000/sample/authorize/", "token_endpoint": "http://localhost:8000/sample/token/", } )
リクエストパラメータの確認
OIDCの仕様でトークンエンドポイントに必要なリクエストパラメータを確認する。以下の通りである。
| パラメータ名 | 説明 |
|---|---|
| redirect_uri | 認可リクエスト時と同じredirect_uriかの検証に使う |
| grant_type | 認可コードフローの場合は固定でauthorization_codeという文字列を渡す |
| code | 認可コードフローの場合は固定でauthorization_codeという文字列を渡す |
| client_id | RPのclient_id |
| client_secret | RPのclient_secret |
認可コードの永続化
まずは、現状はただのランダム文字列でしかない認可コードを永続化して、redirect_uri、client_idを紐づける必要がある。
AuthorizationCodeというModelを定義する。認可コードは実質トークンだから期限もあった方がセキュアだと思うので、expired_atも定義しておく。
models.py
class AuthorizationCode(models.Model): code = models.CharField(max_length=64) redirect_uri = models.CharField(max_length=256) client = models.ForeignKey(RelyingParty, on_delete=models.CASCADE) expired_at = models.DateTimeField()
認可コードを発行しているのは同意画面のPOST処理である。同意画面のPOST処理でclientを参照できる必要があるので、ConsentAccessTokenにclientを追加する。
※ redirect_uriはリダイレクト先のURLとして必要なので既に参照できる状態になってる。
class ConsentAccessToken(models.Model): # ...省略 client = models.ForeignKey(RelyingParty, on_delete=models.CASCADE)
clientを同意画面まで引きずり回す。
forms.py
class LoginForm(forms.Form): # ...省略 client_id = forms.CharField(widget=forms.HiddenInput)
views.py
class AuthorizeView(View): def get(self, request): # ...省略 return render( request, "login.html", { "form": LoginForm( initial={ "scope": " ".join(form.cleaned_data["scope"]), "redirect_uri": form.cleaned_data["redirect_uri"], "state": form.cleaned_data["state"], "client_id": form.cleaned_data["client_id"], } ) }, ) # ...省略 def post(self, request): login(request, user) token = ConsentAccessToken.objects.create( token=crypto.get_random_string(8), user=user, expired_at=datetime.now() + timedelta(minutes=10), redirect_uri=form.cleaned_data["redirect_uri"], state=form.cleaned_data["state"], client=RelyingParty.objects.get(client_id=form.cleaned_data["client_id"]), ) # ...省略
これで同意画面のPOST処理からトークン経由でredirect_uriとclientを参照できる。認可コードの永続化処理を追加する。
views.py
class ConsentView(LoginRequiredMixin, View): # ...省略 def post(self, request, consent_access_token): token = ConsentAccessToken.objects.filter( token=consent_access_token, user=request.user, expired_at__gte=datetime.now(), ).get() # 認可コード発行 code = crypto.get_random_string(8) AuthorizationCode.objects.create( code=code, redirect_uri=token.redirect_uri, client=token.client, expired_at=datetime.now() + timedelta(minutes=10), ) # リダイレクト先URL作成 location_url = f"{token.redirect_uri}?code={code}&state={token.state}" token.delete() return redirect(location_url)
ここまで実装して、コールバックされた後にDjango Admin上からAuthorizationCodeを確認すると必要な値が保存されていることを確認できる。

RPへのclient_secretの追加
RPにclient_secretを追加する。まずはModelにフィールドを追加する。
class RelyingParty(models.Model): # ...省略 client_secret = models.CharField(max_length=256, editable=False)
Django Adminのカスタマイズの話になるので本質ではないが、Django AdminにRPを保存した際に自動でシークレットを生成 & ハッシュ化して保存されるようにする。
Django Adminのカスタマイズについてはakiyokoさんの本に詳しく説明されている。 booth.pm
Modelのフィールドにeditable=FalseをつけるとDjango Adminで追加時に入力対象から除外できる。
https://docs.djangoproject.com/ja/4.2/ref/models/fields/#editable
シークレットはDjangoのUserモデルのパスワードハッシュ化に使う関数でハッシュ化しておく。勉強用なので平文のシークレットは雑にコンソールに出力する。
admin.py
# ...省略 class RelyingPartyAdmin(admin.ModelAdmin): list_display = ["name", "client_id"] def save_model(self, request, obj, form, change): client_secret = crypto.get_random_string(16) print("client_secret:", client_secret) obj.client_secret = make_password(client_secret) return super().save_model(request, obj, form, change) admin.site.register(RelyingParty, RelyingPartyAdmin)


これでトークンエンドポイントを実装する下準備の1つができたはず。今日はここまで。