Last active
December 10, 2018 12:55
-
-
Save ryu22e/4c4ebc48fe6e24a273991616235e9177 to your computer and use it in GitHub Desktop.
WebAuthnサインアップ(サーバーサイド)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""users/forms.py""" | |
from django import forms | |
from .models import User | |
class SignUpForm(forms.ModelForm): | |
def __init__(self, *args, **kwargs): | |
super().__init__(*args, **kwargs) | |
# 「v-model」はVue.jsで使うために書いた。 | |
self.fields['username'].widget.attrs.update({'v-model': 'username'}) | |
self.fields['display_name'].widget.attrs.update( | |
{'v-model': 'displayName'}) | |
class Meta: | |
model = User | |
fields = [ | |
'username', | |
'display_name', | |
] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""users/permissions.py""" | |
from django.middleware.csrf import CsrfViewMiddleware | |
from rest_framework import permissions | |
class CSRFCheck(CsrfViewMiddleware): | |
def _reject(self, request, reason): | |
# Return the failure reason instead of an HttpResponse | |
return reason | |
class CSRFPermission(permissions.BasePermission): | |
"""CSRF対策用のPermission。 | |
SessionAuthenticationにもCSRF対策機能があるが、非ログイン時にCSRFのチェックを行わない仕様なので | |
自分で作った。 | |
SessionAuthenticationの仕様については以下Warningも参照。 | |
https://www.django-rest-framework.org/api-guide/authentication/#sessionauthentication | |
""" | |
def has_permission(self, request, view): | |
check = CSRFCheck() | |
check.process_request(request) | |
reason = check.process_view(request, None, (), {}) | |
return reason is None |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""users/renderers.py""" | |
from fido2 import cbor | |
from rest_framework import renderers | |
class CBORRenderer(renderers.BaseRenderer): | |
media_type = 'application/cbor' | |
format = 'cbor' | |
charset = None | |
render_style = 'binary' | |
def render(self, data, media_type=None, renderer_context=None): | |
return cbor.dumps(data) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""users/serializers.py""" | |
from rest_framework import serializers | |
from .models import User | |
class UserSerializer(serializers.ModelSerializer): | |
class Meta: | |
model = User | |
fields = [ | |
'username', | |
'display_name', | |
] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""users/urls.py""" | |
from django.urls import include, path | |
from . import views | |
app_name = 'users' | |
urlpatterns = [ | |
path('signup/', views.SignUpView.as_view(), name='signup'), | |
path( | |
'apis/', | |
include( | |
[ | |
path('register-begin/', views.RegisterBeginViewSet.as_view(), | |
name='register_begin'), | |
path('register-complete/', views.RegisterCompleteViewSet.as_view(), | |
name='register_complete'), | |
], | |
), | |
), | |
] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""users/views.py""" | |
import pickle | |
from django.conf import settings | |
from django.views import generic | |
from fido2.client import ClientData | |
from fido2.ctap2 import AttestationObject | |
from fido2.server import Fido2Server, RelyingParty | |
from rest_framework.response import Response | |
from rest_framework.views import APIView | |
from .forms import SignUpForm | |
from .models import User, WebAuthnPublicKey | |
from .parsers import CBORParser | |
from .permissions import CSRFPermission | |
from .renderers import CBORRenderer | |
from .serializers import UserSerializer | |
# RELYING_PARTY_DOMAINは開発機なら「localhost」を入れる。 | |
# RELYING_PARTY_NAMEはどんな値でもよい。 | |
rp = RelyingParty(settings.RELYING_PARTY_DOMAIN, settings.RELYING_PARTY_NAME) | |
server = Fido2Server(rp) | |
class SignUpView(generic.TemplateView): | |
"""登録画面を表示させるためのビュー""" | |
template_name = 'users/signup.html' | |
def get_context_data(self, **kwargs): | |
context = super().get_context_data(**kwargs) | |
context['form'] = SignUpForm() | |
return context | |
class RegisterBeginViewSet(APIView): | |
"""PublicKeyCredentialCreateOptionsを返すためのAPI""" | |
serializer_class = UserSerializer | |
renderer_classes = (CBORRenderer,) | |
permission_classes = (CSRFPermission,) | |
def post(self, request, format=None): | |
serializer = self.serializer_class(data=request.data) | |
serializer.is_valid(raise_exception=True) | |
username = serializer.data['username'] | |
display_name = serializer.data['display_name'] | |
# 過去に同じusernameに紐づけた認証器は登録できないようにしている。 | |
credentials = ( | |
WebAuthnPublicKey. | |
objects. | |
credentials(username) | |
) | |
registration_data = server.register_begin({ | |
'id': username.encode('utf-8'), # idはbytes型にする必要がある | |
'name': username, | |
'displayName': display_name, | |
}, credentials) | |
# PickleSerializer以外のセッションシリアライザだと | |
# 「TypeError: Object of type 'bytes' is not JSON serializable」エラーになる。 | |
request.session['challenge'] = ( | |
registration_data['publicKey']['challenge'] | |
) | |
request.session['user'] = { | |
'username': username, | |
'display_name': display_name, | |
} | |
return Response(registration_data) | |
class RegisterCompleteViewSet(APIView): | |
"""登録処理を行うAPI""" | |
parser_classes = (CBORParser,) | |
renderer_classes = (CBORRenderer,) | |
permission_classes = (CSRFPermission,) | |
def post(self, request, format=None): | |
data = request.data[0] | |
client_data = ClientData(data['clientDataJSON']) | |
att_obj = AttestationObject(data['attestationObject']) | |
auth_data = server.register_complete( | |
request.session.pop('challenge'), | |
client_data, | |
att_obj | |
) | |
u = request.session.pop('user') | |
user = User.objects.create_user( | |
username=u['username'], | |
display_name=u['display_name'], | |
) | |
WebAuthnPublicKey.objects.create( | |
user=user, | |
credential_data=pickle.dumps(auth_data.credential_data), | |
) | |
return Response({'status': 'OK'}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment