3

I'm working at the moment on an implementation of webauthn on a project. The main point is to give the possibility to user to use FaceId or fingerprint scan on their mobile on the website.

I tried the djoser version of webauthn but I wanted to give the possibility to user that already have an account so I took the implementation of webauthn of djoser and I updated it to make it working with already created account.

I can ask for the signup request of a webauthn token and create the webauthn token with the front (Angular) where I use @simplewebauthn/browser ("@simplewebauthn/browser": "^6.3.0-alpha.1") . Everything is working fine there.

I use the latest version of djoser by pulling git and the version of webauthn is 0.4.7 linked to djoser.

djoser @git+https://github.com/sunscrapers/djoser.git@abdf622f95dfa2c6278c4bd6d50dfe69559d90c0
webauthn==0.4.7

But when I send back to the backend the result of the registration, I have an error:

Authentication rejected. Error: Invalid signature received..

Here's the SignUpView:

    permission_classes = (AllowAny,)

    def post(self, request, ukey):
        co = get_object_or_404(CredentialOptions, ukey=ukey)

        webauthn_registration_response = WebAuthnRegistrationResponse(
            rp_id=settings.DJOSER["WEBAUTHN"]["RP_ID"],
            origin=settings.DJOSER["WEBAUTHN"]["ORIGIN"],
            registration_response=request.data,
            challenge=co.challenge,
            none_attestation_permitted=True,
        )
        try:
            webauthn_credential = webauthn_registration_response.verify()
        except RegistrationRejectedException as e:
            return Response(
                {api_settings.NON_FIELD_ERRORS_KEY: format(e)},
                status=status.HTTP_400_BAD_REQUEST,
            )
        user = User.objects.get(username=request.data["username"])
        user_serializer = CustomUserSerializer(user)
        co.challenge = ""
        co.user = user
        co.sign_count = webauthn_credential.sign_count
        co.credential_id = webauthn_credential.credential_id.decode()
        co.public_key = webauthn_credential.public_key.decode()
        co.save()


        return Response(user_serializer.data, status=status.HTTP_201_CREATED)

And I based my work on https://github.com/sunscrapers/djoser/blob/abdf622f95dfa2c6278c4bd6d50dfe69559d90c0/djoser/webauthn/views.py#L53

Here's also the SignUpRequesrtView where I edited some little things to make it working the way I want:

class SignupRequestView(APIView):
    permission_classes = (AllowAny,)

    def post(self, request):
        CredentialOptions.objects.filter(username=request.data["username"]).delete()

        serializer = WebauthnSignupSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        co = serializer.save()

        credential_registration_dict = WebAuthnMakeCredentialOptions(
            challenge=co.challenge,
            rp_name=settings.DJOSER["WEBAUTHN"]["RP_NAME"],
            rp_id=settings.DJOSER["WEBAUTHN"]["RP_ID"],
            user_id=co.ukey,
            username=co.username,
            display_name=co.display_name,
            icon_url="",
        )

        return Response(credential_registration_dict.registration_dict)

And I also updated the WebAuthnSignupSerializer to retrieve an check if there's an account with the username given and if yes, create the CredentialOptions:

class WebauthnSignupSerializer(serializers.ModelSerializer):
    class Meta:
        model = CredentialOptions
        fields = ("username", "display_name")

    def create(self, validated_data):
        validated_data.update(
            {
                "challenge": create_challenge(
                    length=settings.DJOSER["WEBAUTHN"]["CHALLENGE_LENGTH"]
                ),
                "ukey": create_ukey(length=settings.DJOSER["WEBAUTHN"]["UKEY_LENGTH"]),
            }
        )
        return super().create(validated_data)

    def validate_username(self, username):
        if User.objects.filter(username=username).exists():
            return username
        else:
            raise serializers.ValidationError(f"User {username} does not exist.")```
Jonathan Ciapetti
  • 1,261
  • 3
  • 11
  • 16
thelittlewozniak
  • 368
  • 1
  • 8
  • 21

2 Answers2

1

EDIT: TL;DR;

@simplewebauthn/browser encodes the signature as base64url while duo-labs/py_webauthn expects a hex encoded signature.


Well, it's not really an answer, but rather a little "assistance".

You can check whether the signature is valid using this little tool (at the bottom of the page): https://webauthn.passwordless.id/demos/playground.html

At least, using that, you will know if your data is correct or if something was stored wrongly. There are so many conversions from bytes to base64url and back that it's not always easy to track. Perhaps it is a data format/convertion issue? Like not converting to bytes, or accidentally double encoding as base64url.

Lastly, the stored public key has a different format depending on the algorithm. Either "raw" or "ASN.1" wrapped, in case you have a problem with the key itself.

Good luck!


EDIT:

While delving a bit into the source code of sunscrapers/djoser, I noticed something quite odd. While all data is encoded as base64, it appears the signature is hex encoded instead, see their test app

That seems to be because it uses duo-labs/py_webauthn as dependency which expects a hex encoded signature. On the other hand, the @simplewebauthn/browser lib encodes it into base64url, like all other data.

dagnelies
  • 5,203
  • 5
  • 38
  • 56
0

The verify() method expects a RegistrationResponse object as an argument, but you're passing it the entire request data. You need to extract the registrationResponse field from the request data and pass that to the verify() method instead.

Change this:

webauthn_registration_response = WebAuthnRegistrationResponse(
rp_id=settings.DJOSER["WEBAUTHN"]["RP_ID"],
origin=settings.DJOSER["WEBAUTHN"]["ORIGIN"],
registration_response=request.data,  # <-- update this line
challenge=co.challenge,
none_attestation_permitted=True,)

To this:

webauthn_registration_response = WebAuthnRegistrationResponse(
rp_id=settings.DJOSER["WEBAUTHN"]["RP_ID"],
origin=settings.DJOSER["WEBAUTHN"]["ORIGIN"],
registration_response=request.data['registrationResponse'],  # <-- update this line
challenge=co.challenge,
none_attestation_permitted=True,)
Sam
  • 19
  • 4