You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
jumpserver/apps/authentication/backends/passkey/fido.py

158 lines
4.9 KiB

import json
from urllib.parse import urlparse
import fido2.features
from django.conf import settings
from django.utils import timezone
from django.utils.translation import gettext as _
from fido2.server import Fido2Server
from fido2.utils import websafe_decode, websafe_encode
from fido2.webauthn import PublicKeyCredentialRpEntity, AttestedCredentialData, PublicKeyCredentialUserEntity
from rest_framework.serializers import ValidationError
from user_agents.parsers import parse as ua_parse
from common.utils import get_logger
from .models import Passkey
logger = get_logger(__name__)
try:
fido2.features.webauthn_json_mapping.enabled = True
except:
pass
def get_current_platform(request):
ua = ua_parse(request.META["HTTP_USER_AGENT"])
if 'Safari' in ua.browser.family:
return "Apple"
elif 'Chrome' in ua.browser.family and ua.os.family == "Mac OS X":
return "Chrome on Apple"
elif 'Android' in ua.os.family:
return "Google"
elif "Windows" in ua.os.family:
return "Microsoft"
else:
return "Key"
def get_server_id_from_request(request, allowed=()):
origin = request.META.get('HTTP_REFERER')
if not origin:
origin = request.get_host()
p = urlparse(origin)
if p.netloc in allowed or p.hostname in allowed:
return p.hostname
else:
return 'localhost'
def default_server_id(request):
domains = list(settings.ALLOWED_DOMAINS)
if settings.SITE_URL:
domains.append(urlparse(settings.SITE_URL).hostname)
return get_server_id_from_request(request, allowed=domains)
def get_server(request=None):
"""Get Server Info from settings and returns a Fido2Server"""
server_id = settings.FIDO_SERVER_ID or default_server_id(request)
if callable(server_id):
fido_server_id = settings.FIDO_SERVER_ID(request)
elif ',' in server_id:
fido_server_id = get_server_id_from_request(request, allowed=server_id.split(','))
else:
fido_server_id = server_id
logger.debug('Fido server id: {}'.format(fido_server_id))
if callable(settings.FIDO_SERVER_NAME):
fido_server_name = settings.FIDO_SERVER_NAME(request)
else:
fido_server_name = settings.FIDO_SERVER_NAME
rp = PublicKeyCredentialRpEntity(id=fido_server_id, name=fido_server_name)
return Fido2Server(rp)
def get_user_credentials(username):
user_passkeys = Passkey.objects.filter(user__username=username)
return [AttestedCredentialData(websafe_decode(uk.token)) for uk in user_passkeys]
def register_begin(request):
server = get_server(request)
user = request.user
user_credentials = get_user_credentials(user.username)
prefix = request.query_params.get('name', '')
prefix = '(' + prefix + ')'
user_entity = PublicKeyCredentialUserEntity(
id=str(user.id).encode('utf8'),
name=user.username + prefix,
display_name=user.name,
)
auth_attachment = getattr(settings, 'KEY_ATTACHMENT', None)
data, state = server.register_begin(
user_entity, user_credentials,
authenticator_attachment=auth_attachment,
resident_key_requirement=fido2.webauthn.ResidentKeyRequirement.PREFERRED
)
request.session['fido2_state'] = state
data = dict(data)
return data, state
def register_complete(request):
if not request.session.get("fido2_state"):
raise ValidationError("No state found")
data = request.data
server = get_server(request)
state = request.session.pop("fido2_state")
auth_data = server.register_complete(state, response=data)
encoded = websafe_encode(auth_data.credential_data)
platform = get_current_platform(request)
name = data.pop("key_name", '') or platform
passkey = Passkey.objects.create(
user=request.user,
token=encoded,
name=name,
platform=platform,
credential_id=data.get('id')
)
return passkey
def auth_begin(request):
server = get_server(request)
credentials = []
username = None
if request.user.is_authenticated:
username = request.user.username
if username:
credentials = get_user_credentials(username)
auth_data, state = server.authenticate_begin(credentials)
request.session['fido2_state'] = state
return auth_data
def auth_complete(request):
server = get_server(request)
data = request.data.get("passkeys")
data = json.loads(data)
cid = data['id']
key = Passkey.objects.filter(credential_id=cid, is_active=True).first()
if not key:
raise ValueError(_("This key is not registered"))
credentials = [AttestedCredentialData(websafe_decode(key.token))]
state = request.session.get('fido2_state')
server.authenticate_complete(state, credentials=credentials, response=data)
request.session["passkey"] = '{}_{}'.format(key.id, key.name)
key.date_last_used = timezone.now()
key.save(update_fields=['date_last_used'])
return key.user