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
相关产品推荐
相关产品推荐

