我目前正在一个项目上实现web认证。主要目的是让用户有可能在网站上使用手机上的FaceId或指纹扫描。
我尝试了djoser版本的webauthn,但我想给已经拥有帐户的用户提供可能性,所以我采用了djoser的webauthn的实现,并更新了它,使其与已经创建的帐户一起工作。
我可以请求webauthn令牌的注册请求,并使用前面(Angular)创建webauthn令牌,其中我使用@simplewebauthn/browser (@simplewebauthn/browser"; "^6.3.0-alpha.1")。那里一切都很好。
我使用最新版本的djoser通过git和webauthn的版本是0.4.7链接到djoser。
djoser @git+https://github.com/sunscrapers/djoser.git@abdf622f95dfa2c6278c4bd6d50dfe69559d90c0
webauthn==0.4.7
但是当我将注册结果发送回后端时,我有一个错误:
Authentication rejected. Error: Invalid signature received..
这是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)
我的工作基于https://github.com/sunscrapers/djoser/blob/abdf622f95dfa2c6278c4bd6d50dfe69559d90c0/djoser/webauthn/views.py#L53
这里也是SignUpRequesrtView
,我编辑了一些小的东西,使它的工作方式,我想:
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)
我还更新了WebAuthnSignupSerializer
以检索并检查是否有给定用户名的帐户,如果是,则创建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.")```
EDIT: TL;
@simplewebauthn/browser将签名编码为base64url,而duo-labs/py_webauthn期望十六进制编码的签名。
嗯,这不是一个真正的答案,而是一个小小的"帮助"。
您可以使用这个小工具(在页面底部)检查签名是否有效:https://webauthn.passwordless.id/demos/playground.html
至少,使用它,您将知道您的数据是正确的,或者如果某些内容存储错误。从字节到base64url的转换如此之多,以至于跟踪起来并不容易。也许是数据格式/转换的问题?比如不转换为字节,或者不小心将编码转换为base64url。
最后,根据算法的不同,存储的公钥具有不同的格式。要么"raw"或";ASN.1"如果密钥本身有问题,可以将其封装。
祝你好运!
编辑:在深入研究sunscraper/djoser的源代码时,我注意到一些非常奇怪的事情。虽然所有数据都被编码为base64,但似乎签名是十六进制编码的,参见他们的测试应用
这似乎是因为它使用duo-labs/py_webauthn作为依赖项,期望十六进制编码签名。另一方面,@simplewebauthn/browser库将其编码为base64url,就像所有其他数据一样。
verify()方法需要一个RegistrationResponse对象作为参数,但是您将整个请求数据传递给它。您需要从请求数据中提取registrationResponse字段,并将其传递给verify()方法。
改变:
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,)
:
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,)