我们最近使用 Django 2.1.7 将 WebAuthn 集成到我们的系统中。对于后端来说, 我不确定问题是什么。它在计算机和我们测试过的各种 iOS 设备上完美运行。我有一台配备 Android 14 和最新可用版本的 Chrome 的三星 S23。它还在 POCO 上进行了测试,并且返回了相同的负面结果。
我正在使用 WebAuthn 2.2.0,这就是我拥有端点的方式:
报名:
接受挑战:
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)
这很有效,因为我在 Android 手机上生成的密钥存储在我的服务器上。
这就是我使用服务的方式:我正在使用 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);
}
}
这些是我的登录代码:
接受挑战:
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)后,我收到消息说没有注册密钥(我在更改服务后注册了密钥)。
很难根据该代码准确判断浏览器中最终会出现哪些 WebAuthn 选项,但请确保在创建过程中将
authenticatorSelection.requireResidentKey
spec 设置为 true。 Android 支持可发现和不可发现的凭据,而您需要前者。
如果创建后仍未发现凭据,请在 Google 密码管理器中检查它是否确实具有凭据。 (它隐藏在 Android 设置中的“密码和帐户”下。您还可以在桌面上的 Chrome 中登录同一 Google 帐户,然后查看 chrome://password-manager。)