パッケージは 2 要素 fido2 認証を行います。1 ステップ認証でも、ユーザーはユーザー名を指定する必要があります。
このパッケージを使用してパスワードレス認証を実行するにはどうすればよいですか?
パッケージは 2 要素 fido2 認証を行います。1 ステップ認証でも、ユーザーはユーザー名を指定する必要があります。
このパッケージを使用してパスワードレス認証を実行するにはどうすればよいですか?
私の知る限り、パスワードなしで実行するには、資格情報を で検索できる必要がありますがrpId
、django-fido
パッケージは現在、資格情報を yubikey に保存せずにキーを登録しています。
クレデンシャルを yubikey に保存するには、resident_key=True
内に埋め込まれたものを設定する必要がありviews.py
ます。この質問と回答を参照してください
パッケージ リポジトリで機能リクエストを作成して、resident_key
パッケージ内のコードを編集して有効にすると、ツールをresident_key=True
使用して資格情報を一覧表示することで確認できます。ykman
ykman fido credentials list
そして、これに似た資格情報がリストされているのを見ることができるはずです
Jamess-MacBook-Pro:tigerpaw_webui jlin$ ykman fido credentials list
Enter your PIN:
demo.yubico.com c5bcb2d737f91739151e150a942928fb6c5d00d6bb8380475efdbc2761a3xxxx jlin
localhost 6c6461705f6a616d65732e6cxxxx ldap_james.lin
以下のパスワードレス認証を容易にするために、いくつかのコード (ほとんどはパッケージの views.py から借用) を API スタイルでハックしました。
以下のユーザーを検索する方法は、資格情報 ID を使用することに注意してください。docnavigator.crendentials.get()
に従ってuser.id (userHandle from ) を使用するには、私のPRをマージする必要があります
import base64
from http.client import BAD_REQUEST
from typing import Tuple, Dict
from django.contrib.auth import authenticate, login
from django.contrib.auth.base_user import AbstractBaseUser
from django.utils.encoding import force_text
from django_fido.views import Fido2ViewMixin, Fido2ServerError
from django.utils.translation import gettext_lazy as _
from fido2.client import ClientData
from fido2.ctap2 import AuthenticatorData
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.request import Request
from rest_framework.exceptions import ValidationError
from rest_framework import serializers
class FidoAuthenticationSerializer(serializers.Serializer):
client_data = serializers.CharField()
credential_id = serializers.CharField()
authenticator_data = serializers.CharField()
signature = serializers.CharField()
def validate_client_data(self, value) -> ClientData:
"""Return decoded client data."""
try:
return ClientData(base64.b64decode(value))
except ValueError:
raise ValidationError(_('FIDO 2 response is malformed.'), code='invalid')
def validate_credential_id(self, value) -> bytes:
"""Return decoded credential ID."""
try:
return base64.b64decode(value)
except ValueError:
raise ValidationError(_('FIDO 2 response is malformed.'), code='invalid')
def validate_authenticator_data(self, value) -> AuthenticatorData:
"""Return decoded authenticator data."""
try:
return AuthenticatorData(base64.b64decode(value))
except ValueError:
raise ValidationError(_('FIDO 2 response is malformed.'), code='invalid')
def validate_signature(self, value) -> bytes:
"""Return decoded signature."""
try:
return base64.b64decode(value)
except ValueError:
raise ValidationError(_('FIDO 2 response is malformed.'), code='invalid')
class PasswordlessAuthRequestView(Fido2ViewMixin, APIView):
authentication_classes = []
permission_classes = []
def create_fido2_request(self) -> Tuple[Dict, Dict]:
"""Create and return FIDO 2 authentication request.
@raise ValueError: If request can't be created.
"""
return self.server.authenticate_begin([], user_verification=self.user_verification)
def get(self, request: Request) -> Response:
"""Return JSON with FIDO 2 request."""
try:
request_data, state = self.create_fido2_request()
except ValueError as error:
return Response({
'error_code': getattr(error, 'error_code', Fido2ServerError.DEFAULT),
'message': force_text(error),
'error': force_text(error), # error key is deprecated and will be removed in the future
}, status=BAD_REQUEST)
# Encode challenge into base64 encoding
challenge = request_data['publicKey']['challenge']
challenge = base64.b64encode(challenge).decode('utf-8')
request_data['publicKey']['challenge'] = challenge
# Encode credential IDs, if exists - registration
if 'excludeCredentials' in request_data['publicKey']:
encoded_credentials = []
for credential in request_data['publicKey']['excludeCredentials']:
encoded_credential = credential.copy()
encoded_credential['id'] = base64.b64encode(encoded_credential['id']).decode('utf-8')
encoded_credentials.append(encoded_credential)
request_data['publicKey']['excludeCredentials'] = encoded_credentials
# Encode credential IDs, if exists - authentication
if 'allowCredentials' in request_data['publicKey']:
encoded_credentials = []
for credential in request_data['publicKey']['allowCredentials']:
encoded_credential = credential.copy()
encoded_credential['id'] = base64.b64encode(encoded_credential['id']).decode('utf-8')
encoded_credentials.append(encoded_credential)
request_data['publicKey']['allowCredentials'] = encoded_credentials
# Store the state into session
self.request.session[self.session_key] = state
return Response(request_data)
class PasswordlessAuthView(Fido2ViewMixin, APIView):
authentication_classes = []
permission_classes = []
def post(self, request, *args, **kwargs):
serializer = FidoAuthenticationSerializer(data=request.data)
serializer.is_valid()
user = self.complete_authentication(serializer.validated_data)
login(request, user, 'btg_auth_pp.backends.PasswordlessAuthenticationBackend')
return Response(response_payload)
def complete_authentication(self, data) -> AbstractBaseUser:
"""
Complete the authentication.
@raise ValidationError: If the authentication can't be completed.
"""
state = self.request.session.pop(self.session_key, None)
if state is None:
raise ValidationError(_('Authentication request not found.'), code='missing')
fido_kwargs = dict(
fido2_server=self.server,
fido2_state=state,
fido2_response=data,
)
user = authenticate(request=self.request, **fido_kwargs)
if user is None:
raise ValidationError(_('Authentication failed.'), code='invalid')
return user
import base64
import logging
from typing import Any, Dict, Optional
from django.contrib import messages
from django.contrib.auth import get_backends
from django.contrib.auth.base_user import AbstractBaseUser
from django.core.exceptions import PermissionDenied
from django.http import HttpRequest
from fido2.server import Fido2Server
from django_fido.models import Authenticator
from django.utils.translation import gettext_lazy as _
def is_fido_backend_used() -> bool:
"""Detect whether FIDO2 authentication backend is used."""
for auth_backend in get_backends():
if isinstance(auth_backend, (PasswordlessAuthenticationBackend,)):
return True
return False
class PasswordlessAuthenticationBackend(object):
"""
Authenticate user using FIDO 2.
@cvar counter_error_message: Error message in case FIDO 2 device counter didn't increase.
"""
counter_error_message = _("Counter of the FIDO 2 device decreased. Device may have been duplicated.")
def authenticate(self, request: HttpRequest, fido2_server: Fido2Server,
fido2_state: Dict[str, bytes], fido2_response: Dict[str, Any]) -> Optional[AbstractBaseUser]:
"""Authenticate using FIDO 2."""
credential_id_data = base64.b64encode(fido2_response['credential_id']).decode('utf-8')
authenticator = Authenticator.objects.get(credential_id_data=credential_id_data)
user = authenticator.user
credentials = [authenticator.credential]
try:
credential = fido2_server.authenticate_complete(
fido2_state, credentials, fido2_response['credential_id'], fido2_response['client_data'],
fido2_response['authenticator_data'], fido2_response['signature'])
except ValueError as error:
_LOGGER.info("FIDO 2 authentication failed with error: %r", error)
return None
device = user.authenticators.get(credential_id_data=base64.b64encode(credential.credential_id).decode('utf-8'))
try:
self.mark_device_used(device, fido2_response['authenticator_data'].counter)
except ValueError:
# Raise `PermissionDenied` to stop the authentication process and skip remaining backends.
messages.error(request, self.counter_error_message)
raise PermissionDenied("Counter didn't increase.")
return user
def mark_device_used(self, device, counter):
"""Update FIDO 2 device usage information."""
if counter == 0 and device.counter == 0:
# Counter is unsupported by the device, bail out early
return
if counter <= device.counter:
_LOGGER.info("FIDO 2 authentication failed because of not increasing counter.")
raise ValueError("Counter didn't increase.")
device.counter = counter
device.full_clean()
device.save()
def get_user(self, user_id):
"""Return user based on its ID."""
try:
return get_user_model().objects.get(pk=user_id)
except get_user_model().DoesNotExist:
return Non
import React from 'react';
import {Button} from 'react-bootstrap';
import AuthAPI from '@/js/api/auth';
const FidoForm = ({onSuccess}) => {
const base64ToArrayBuffer = (base64) => {
const binaryString = window.atob(base64);
const bytes = new Uint8Array(binaryString.length)
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i)
}
return bytes
}
const arrayBufferToBase64 = (buffer) => {
let binary = ''
const bytes = new Uint8Array(buffer)
for (const byte of bytes)
binary += String.fromCharCode(byte)
return window.btoa(binary)
}
const onFidoSubmit = (formData) => {
AuthAPI.fidoTwoStepAuthRequest().then(
data => {
const publicKey = data.publicKey;
publicKey.challenge = base64ToArrayBuffer(publicKey.challenge)
// Decode credentials
const decodedCredentials = []
for (const credential of publicKey.allowCredentials){
credential.id = base64ToArrayBuffer(credential.id)
decodedCredentials.push(credential)
}
publicKey.allowCredentials = decodedCredentials;
navigator.credentials.get({ publicKey }).then(result => {
const authData = {
client_data: arrayBufferToBase64(result.response.clientDataJSON),
credential_id: arrayBufferToBase64(result.rawId),
authenticator_data: arrayBufferToBase64(result.response.authenticatorData),
signature: arrayBufferToBase64(result.response.signature)
}
AuthAPI.fidoTwoStepAuthenticate(authData).then(resp=>onSuccess(resp.token));
});
}
);
}
return (
<div>
<Button onClick={onFidoSubmit}>Login with YUBI key</Button>
</div>
);
};
export default FidoForm;