该软件包执行两因素 fido2 身份验证,即使是一步身份验证也需要用户指定用户名。
如何使用这个包进行无密码认证?
据我所知,要执行无密码,凭证需要能够通过 查找rpId
,但是django-fido
包当前注册密钥而不将凭证存储到 yubikey。
为了将凭证存储到 yubikey,我们需要设置resident_key=True
哪个埋在里面views.py
,看这个问题和答案
我在包 repo 上创建了一个功能请求以允许设置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
我已经以 API 风格破解了一些代码(主要是从包的 views.py 中借来的),以方便下面的无密码身份验证
请注意,我在下面查找用户的方式是通过凭据 ID。navigator.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;