October 22, 2024
Chicago 12, Melborne City, USA
Android

WebAuthn doesn't work on Chrome Android but works on Desktop and iOS


We recently integrated WebAuthn into our system using Django 2.1.7. For the backend,
I’m not sure what the problem could be. It works perfectly on computers and on various iOS devices where we tested it. I have a Samsung S23 with Android 14 and the latest available version of Chrome. It was also tested on a POCO, and it returned the same negative result.

I’m using WebAuthn 2.2.0, and this is how I have my endpoints:

Registration:

Get challenge:

def get_challenge_authn(request):
    try:
        rp_id     = "localhost"

        registration_options = generate_registration_options(
            rp_id     = rp_id,
            rp_name   = "MySite",
            user_name = user.username,
            user_display_name=user.username
        )
        
        if not registration_options:
            return JsonResponse({'status': 'error', 'msg': 'Ocurrio un error. Contacte a soporte'}, status=500)

        # Almacena temporalmente el challenge en la sesión del usuario.
        request.session['webauthn_challenge'] = base64.urlsafe_b64encode(registration_options.challenge).decode("utf-8").rstrip("=") 
        return JsonResponse(options_to_json(registration_options), safe=False)
    except Exception as e:
        return JsonResponse({'status': 'error', 'msg': e}, status=500)
    

Validate and store the registration:

def verificar_guardar_registro_authn(request):    
    body = json.loads(request.body)
    try:

        credential_data = {
            "rawId": body['rawId'],
            "response": {
                "clientDataJSON": body['response']['clientDataJSON'],
                "attestationObject": body['response']['attestationObject'],
            },
            "type": body["type"],
            "id": body["id"],
        }
        stored_challenge = request.session.get('webauthn_challenge')
    
        if not stored_challenge:
            return JsonResponse({'status': 'error', 'msg': 'El challenge no se encuentra.'}, status=500)

        expected_rp_id     = "localhost"
        expected_origin    = "http://localhost:8000"

        verification = verify_registration_response(
            credential         = credential_data,
            expected_challenge = base64url_to_bytes(stored_challenge),
            expected_rp_id     = expected_rp_id,
            expected_origin    = expected_origin,
            require_user_verification = True
        )

        if verification.user_verified:
            credential = webAuthnCredential.objects.create(
                usuario       = request.user,
                credential_id = base64.urlsafe_b64encode(verification.credential_id).decode("utf-8").rstrip("="),
                public_key    = base64.urlsafe_b64encode(verification.credential_public_key).decode("utf-8").rstrip("="),
                sign_count    = verification.sign_count
            )

            return JsonResponse({'status': 'ok'}, status=201)
        else:
            return JsonResponse({'status': 'error', 'msg': 'Registro invalido'}, status=400)
    except Exception as e:
        print(traceback.format_exc())
        return JsonResponse({'status': 'error', 'msg': e}, status=500)

This works well, as the keys I generate on my Android phone are stored on my server.

And this is how I consume the services: I’m using the SimpleWebAuthn library

const registrar_autentificacion = async () => {
    try {
        const url = "{% url 'autentificacion:get_challenge_authn' %}";

        const response = await fetch(url);

        const options  = await response.json();
        const attResponse = await SimpleWebAuthnBrowser.startRegistration(JSON.parse(options));
        

        const url_verificacion = "{% url 'autentificacion:verificar_guardar_registro_authn' %}";

        const verificationResponse = await fetch(url_verificacion, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'X-CSRFToken': '{{csrf_token}}'
          },
          body: JSON.stringify(attResponse)
        });

        if (verificationResponse.ok) {
          $('#modal_aviso_upload').modal('show');
          console.log('Registro exitoso.');
        } else {
          mensaje_error.innerText = "Ocurrio un error durante la verificación de la llave."
          mensaje_error.style.display = ''
          console.log('Error en la verificación.');
        }
    } catch (err) {
      mensaje_error.innerText = `Ocurrio un error durante la verificación de la llave.\nError: ${err}`;
      mensaje_error.style.display = ''
      console.error('Error en el registro:', err);
    }
  }

These are my codes for the login:

Get challenge:

def get_challenge_authn_authentication(request):
    try:
        rp_id     = "localhost"

        authentication_options = generate_authentication_options(
            rp_id=rp_id,
            user_verification=UserVerificationRequirement.REQUIRED
        )
        request.session['webauthn_challenge'] = base64.urlsafe_b64encode(authentication_options.challenge).decode("utf-8").rstrip("=")
        return JsonResponse(options_to_json(authentication_options), safe=False)
    except Exception as e:
        print(e)
        return JsonResponse({'error': 'Error', 'msg': e}, safe=False, status=400)

And the verification (although I think this part of the code is no longer reached):

def verificar_guardar_autenticacion(request):
    auth_data = json.loads(request.body)

    stored_challenge = request.session.get('webauthn_challenge')
    if not stored_challenge:
        return JsonResponse({"error": "Challenge no encontrado en sesión"}, status=500)

    credential_id  = auth_data['id']
    credential = webAuthnCredential.objects.filter(credential_id=credential_id).first()

    if not credential:
        return JsonResponse({"error": "No se a encontrado la credencial de llave"}, status=404)

    expected_rp_id     = "localhost"
    expected_origin    = "http://localhost:8000"
    try:
        verification = verify_authentication_response(
            credential          = auth_data,
            expected_rp_id      = expected_rp_id, 
            expected_origin     = expected_origin,
            expected_challenge  = base64url_to_bytes(stored_challenge),
            credential_public_key = base64url_to_bytes(credential.public_key), 
            credential_current_sign_count = credential.sign_count
        )
        if verification.new_sign_count > credential.sign_count:
            credential.sign_count = verification.new_sign_count
            credential.save()

        # Login here
        return JsonResponse({"status": "Autenticación exitosa", 'autenticado': True})

    except Exception as e:
        print(traceback.format_exc())
        return JsonResponse({"error": "Fallo en la autenticación", "msg": str(e)}, status=400)

Here’s the code I have for login:

btn_login?.addEventListener('click', async (e) => {
        e.preventDefault();
        try {
            const url = "{% url 'autentificacion:get_challenge_authn_authentication' %}"
        
            const response = await fetch(url);
            const options = await response.json();

            const assertionResponse = await SimpleWebAuthnBrowser.startAuthentication(JSON.parse(options));

            const url_verificacion = "{% url 'autentificacion:verificar_guardar_autenticacion' %}";

            const verificationResponse = await fetch(url_verificacion, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRFToken': '{{csrf_token}}'
                },
                body: JSON.stringify(assertionResponse)
                });
            
            const result = await verificationResponse.json();
            const { autenticado = false, error="", msg = '' } = result;
            if (autenticado) {
                window.location.href = "/";
            } else {
                mensaje_error.innerText     = `Ocurrio un error durante el inicio de sesión.\n${error}: ${msg}`;
                mensaje_error.style.display = ''
                console.log('Error en la verificación.');
            }

        } catch (error) {
            mensaje_error.innerText     = `Ocurrio un error durante el inicio de sesión.\nError: ${error}`;
            mensaje_error.style.display = ''
            console.error('Error en la autenticación:', error);
        }
    });

When trying to log in, I previously got this message directly:

Error message

After I changed my preferred service (I initially had Samsung Pass, and now I set it to Google), I get the message that there are no registered keys (I registered a key after changing the service).

No keys



You need to sign in to view this answers

Leave feedback about this

  • Quality
  • Price
  • Service

PROS

+
Add Field

CONS

+
Add Field
Choose Image
Choose Video