You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WebAuthn在Chrome Android端登录失败,桌面及iOS端正常

WebAuthn登录在Android 14 Chrome中失效问题

我们基于Django 2.1.7集成了WebAuthn 2.2.0,系统在桌面端及各类iOS设备上运行正常,但在Android 14系统的最新版Chrome浏览器(测试设备为三星S23、POCO)中登录功能失效。

注册流程可正常完成,Android设备生成的密钥已存储至服务器,但登录时出现异常:

  • 使用Samsung Pass作为首选服务时直接报错;
  • 切换为Google服务后,提示“无已注册密钥”(切换后已重新注册密钥)。

注册相关端点

获取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)

验证并存储注册信息

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)

前端注册调用代码(使用SimpleWebAuthn库)

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);
    }
  }

登录相关端点

获取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)

验证登录信息

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)

前端登录调用代码

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);
        }
    });

登录错误表现

  • 使用Samsung Pass作为首选服务时,直接弹出错误提示;
  • 切换为Google服务后,提示“无已注册密钥”(切换后已重新注册密钥)。

内容的提问来源于stack exchange,提问作者Leonardo Esquivel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 00:47:02