以下の続き
ユーザー情報提供同意画面
ユーザー情報提供同意画面(以降「同意画面」とする)を実装する。
Viewの実装
同意画面のViewと雛形のテンプレートを作成して、urlを追加する。
views.py
class ConsentView(View): def get(self, request, authorization_token): return render(request, "consent.html")
consent.html
<html> <head> <title>ユーザー情報提供同意画面</title> </head> <body> <form action="{% url 'sampleapp:authorize' %}" method="post"> {% csrf_token %} <h1>ログイン</h1> {{ form.as_p }} <input type="submit" value="登録"> </form> </body> </html>
urls.py
path("consent/", views.ConsentView.as_view(), name="consent"),
ログイン成功後に同意画面にリダイレクトする。
class AuthorizeView(View): # ...省略 def post(self, request): # ...省略 login(request, user) return redirect("sampleapp:consent")
これでログインに成功したら同意画面が表示されるようになった。
ただし以下の課題がある。
- ログイン画面を経由して同意画面に辿り着いたことを担保していない
- 提供対象のユーザー情報を表示するために
scope
パラメータの値が必要だが、ConsentView
はその値を保持していない。
→ ログイン成功時に同意画面アクセス用のトークンを発行する必要がありそうである。
同意画面表示用トークンの発行
まずはModelを定義する。トークン文字列を保持するモデル本体と、それに対して紐づくscopeのモデルを定義する。無期限はセキュリティ的に良くないと思うので、expired_at
もつけておく。
※ 本当はIdP側が許可しているスコープマスタがあった方がいいのだろうが、今は作らない。
class ConsentAccessToken(models.Model): token = models.CharField(max_length=64, unique=True) user = models.ForeignKey(User, on_delete=models.CASCADE) expired_at = models.DateTimeField() class ConsentAccessTokenScope(models.Model): token = models.ForeignKey(ConsentAccessToken, on_delete=models.CASCADE) scope = models.CharField(max_length=32)
ログイン画面でPOSTする際に、現状ではID/パスワードしか送信していないが、scope
も送信する必要がある。
従って、LoginForm
のフィールドにscope
をhidden属性で追加する
forms.py
class LoginForm(forms.Form): username = forms.CharField(max_length=32) password = forms.CharField(widget=forms.PasswordInput) scope = forms.CharField(widget=forms.HiddenInput) def clean_scope(self): scope = self.cleaned_data["scope"].split() if "openid" not in scope: raise forms.ValidationError("Invalid scope") return scope
ログイン画面のテンプレートにformをセットする際に、scopeの値を引き回す。
class AuthorizeView(View): def get(self, request): form = AuthorizeForm(request.GET) if not form.is_valid(): return HttpResponseBadRequest() client_id = form.cleaned_data["client_id"] if not RelyingParty.objects.filter(client_id=client_id).exists(): return HttpResponseBadRequest() return render(request, "login.html", {"form": LoginForm( initial={"scope": " ".join(form.cleaned_data["scope"])} )})
ログイン画面にhiddenでscope
が引きまわされていることを確認できる。
ログイン成功時にトークンを発行して、同意画面にリダイレクトする際のパスパラメータに付与する。
views.py
class AuthorizeView(View): # ...省略 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), ) for scope in form.cleaned_data["scope"]: ConsentAccessTokenScope.objects.create( token=token, scope=scope ) return redirect("sampleapp:consent", consent_access_token= token. token)
同意画面のパスパラメータにconsent_access_token
を追加する。
urls.py
path( "consent/<str:consent_access_token>/", views.ConsentView.as_view(), name="consent", ),
Viewにconsent_access_token
を受け取る処理を追加する。
テンプレート側でurl
でformのactionのURLを生成しているところにもconsent_access_token
を渡してあげないとNoReverseMatch
が発生する。
views.py
class ConsentView(View): def get(self, request, consent_access_token): return render( request, "consent.html", {"consent_access_token": consent_access_token} )
consent.html
<html> <head> <title>Company Registration</title> </head> <body> <form action="{% url 'sampleapp:consent' consent_access_token %}" method="post"> {% csrf_token %} <h1>以下の提供に同意しますか?</h1> <input type="submit" value="同意"> </form> </body> </html>
ここまででURLのパスパラメータにトークンが入った状態で同意画面を表示できるようになった。
提供対象のユーザー情報を表示する
トークンで永続化したことで、同意画面でscope
を参照できるようになったので、それを元に提供対象のユーザー情報を表示する。
views.py
パスパラメータのトークン文字列からトークンオブジェクトを取得する。期限のチェックとユーザーの照合も取得時に同時に行う。
from django.contrib.auth.mixins import LoginRequiredMixin # ...省略 class ConsentView(LoginRequiredMixin, View): def get(self, request, consent_access_token): token = ConsentAccessToken.objects.filter( token=consent_access_token, user=request.user, expired_at__gte=datetime.now(), ).get() return render(request, "consent.html", {"consent_access_token": token})
画面上ではトークンオブジェクトからスコープを取得する。今回はミニマムでスコープにemail
が存在する場合に提供対象に「メールアドレス」と表示するのみの実装とする。
consent.html
<h1>以下の提供に同意しますか?</h1> {% for scope in consent_access_token.consentaccesstokenscope_set.all %} {% if scope.scope == "email" %} <input type="checkbox" name="scope" value="{{ scope }}" checked>メールアドレス <br> {% endif %} {% endfor %}
これで同意画面の表示までできた。
今日はここまで。 次は同意後に認可コードを発行する処理を実装していく。