0

该软件包执行两因素 fido2 身份验证,即使是一步身份验证也需要用户指定用户名。

如何使用这个包进行无密码认证?

4

1 回答 1

0

据我所知,要执行无密码,凭证需要能够通过 查找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

API

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;
于 2021-11-04T03:40:58.080 回答