Skip to content

Instantly share code, notes, and snippets.

@ryu22e
Last active December 10, 2018 12:55
Show Gist options
  • Save ryu22e/4c4ebc48fe6e24a273991616235e9177 to your computer and use it in GitHub Desktop.
Save ryu22e/4c4ebc48fe6e24a273991616235e9177 to your computer and use it in GitHub Desktop.
WebAuthnサインアップ(サーバーサイド)
"""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',
]
"""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
"""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)
"""users/serializers.py"""
from rest_framework import serializers
from .models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = [
'username',
'display_name',
]
"""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'),
],
),
),
]
"""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