以下の内容はhttps://kamatimaru.hatenablog.com/entry/2024/12/30/192340より取得しました。


勉強のためにOpen ID ConnectのIDプロバイダー側をDjangoで実装する⑤

以下の続き 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_uriclient_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を参照できる必要があるので、ConsentAccessTokenclientを追加する。 ※ 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_uriclientを参照できる。認可コードの永続化処理を追加する。

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つができたはず。今日はここまで。




以上の内容はhttps://kamatimaru.hatenablog.com/entry/2024/12/30/192340より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14