以下の内容はhttps://kamatimaru.hatenablog.com/entry/2025/01/02/172356より取得しました。


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

以下の続き

kamatimaru.hatenablog.com

トークンエンドポイント

認可コードにユーザー情報を紐づける

IDトークンの作成を実装していく。

IDトークンにはユーザー情報を含めるので、まずは認可コードにユーザー情報を紐づける必要がある。

同意画面のPOST処理で認可コードを発行する際に、ユーザー情報も保存する。

models.py

from django.contrib.auth.models import User
# ...省略

class AuthorizationCode(models.Model):
    # ...省略
    user = models.ForeignKey(User, on_delete=models.CASCADE)

views.py

class ConsentView(LoginRequiredMixin, View):
    # ...省略

    def post(self, request, consent_access_token):
        # 省略

        # 認可コード発行
        code = crypto.get_random_string(8)
        AuthorizationCode.objects.create(
            # ...省略
            user=request.user,
        )

        # ...省略
        return redirect(location_url)

# ...省略
class TokenView(View):
    def post(self, request):
        # ...省略
        try:
            authorization_code = AuthorizationCode.objects.get(
                code=form.cleaned_data["code"],
                client=relying_party,
                expired_at__gte=datetime.now(),
            )
        # ...省略
        print("user_id:", authorization_code.user.id)
        return JsonResponse({})

以下のコマンドを実行するとuser_id: 1というprintデバッグ結果が出力された。

curl -X POST "http://localhost:8000/sample/token/" -d "redirect_uri=http://localhost/callback" -d "grant_type=authorization_code" -d "code=ciHSEjCT" -d "client_id=test" -d "client_secret=rWnxR02LuJMeTuFe"

IDトークンのJSONデータを作成する

トークンエンドポイントでユーザー情報を参照できるようになったので、IDトークンのJSONデータを作成する。必要な項目は以下である。

https://openid-foundation-japan.github.io/openid-connect-core-1_0.ja.html#IDToken

フィールド名 説明
iss IdPのURL
sub ユーザーの識別子
aud RPのクライアントID
nonce 認可リクエスト時のリクエストパラメータのnonce
exp IDトークンの有効期限
iat IDトークンが発行された時刻

認可リクエスト時のリクエストパラメータのnonceトークンエンドポイントまで引き回す必要があるので、その部分を実装する。

まずは認証画面用のトークンにnonceを保存する。

forms.py

# ...省略
class LoginForm(forms.Form):
    # ...省略
    nonce = forms.CharField(widget=forms.HiddenInput)

models.py

# ...省略
class ConsentAccessToken(models.Model):
    # ...省略
    nonce = models.CharField(max_length=32)

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"],
                        "nonce": form.cleaned_data["nonce"],
                    }
                )
            },
        )

    def post(self, request):
        # ...省略
        token = ConsentAccessToken.objects.create(
            # ...省略
            nonce=form.cleaned_data["nonce"],
        )
        for scope in form.cleaned_data["scope"]:
            ConsentAccessTokenScope.objects.create(token=token, scope=scope)
        return redirect("sampleapp:consent", consent_access_token=token.token)

ここまでで同意画面のPOST処理でトークンからnonceを参照できるようになった。次に認可コードにnonceを保存する。

models.py

class AuthorizationCode(models.Model):
    # ...省略
    nonce = models.CharField(max_length=32)

views.py

class ConsentView(LoginRequiredMixin, View):
    # ...省略

    def post(self, request, consent_access_token):
        token = ConsentAccessToken.objects.filter(
            # ...省略
        ).get()

        # 認可コード発行
        code = crypto.get_random_string(8)
        AuthorizationCode.objects.create(
            # ...省略
            nonce=token.nonce,
        )

これでトークンエンドポイントでnonceを参照できるようになった。下準備が整ったのでJSONを実装していく。

issの値はIdPのベースURLらしいので、http://localhost:8000とする。

views.py

class TokenView(View):
    def post(self, request):
         # 省略
        id_token_data = {
            "iss": "http://localhost:8000",
        }
        return JsonResponse({})

subの値はユーザー識別子なので、DjangoのユーザーモデルのIDを入れる。

views.py

class TokenView(View):
    def post(self, request):
         # 省略
        id_token_data = {
            # ...省略
            "sub": authorization_code.user.id,
        }
        return JsonResponse({})

audの値にはRPのclient_idを入れる。

views.py

class TokenView(View):
    def post(self, request):
         # 省略
        id_token_data = {
            # ...省略
            "aud": relying_party.client_id,
        }
        return JsonResponse({})

先ほどトークンエンドポイントまで引き回せるようにしたnonceを入れる。

views.py

class TokenView(View):
    def post(self, request):
         # 省略
        id_token_data = {
            # ...省略
            "nonce": authorization_code.nonce,
        }
        return JsonResponse({})

expiatはそれぞれトークンの期限と発行した時間を入れる。

公式ドキュメントに「この値は UTC 1970-01-01T0:0:0Z から該当時刻までの秒数を示す JSON 数値である.」とあり、恐らくUNIXタイムのことを言っているのだと思うので、UNIXタイム形式にする。

expは適当に発行から10分後にしておく。

class TokenView(View):
    def post(self, request):
         # 省略
        id_token_data = {
            # ...省略
            "exp": int((datetime.now() + timedelta(minutes=10)).timestamp()),
            "iat": int(datetime.now().timestamp()),
        }

id_token_dataをprintデバッグする。

class TokenView(View):
    def post(self, request):
         # 省略
        id_token_data = {
            "iss": "http://localhost:8000",
            "sub": authorization_code.user.id,
            "aud": relying_party.client_id,
            "nonce": authorization_code.nonce,
            "exp": int((datetime.now() + timedelta(minutes=10)).timestamp()),
            "iat": int(datetime.now().timestamp()),
        }
        print("id_token_data:", id_token_data)

トークンエンドポイントにリクエストしたら以下のような辞書データが出力されたのでOK。

{
  'iss': 'http://localhost:8000',
  'sub': 1,
  'aud': 'test',
  'nonce': 'efgh',
  'exp': 1735806505,
  'iat': 1735805905
}

次は署名をやっていく。




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

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