Files
notifications-admin/app/main/views/webauthn_credentials.py
Leo Hemsted c26a596839 allow sign in via webauthn credentials
The flow of the code is roughly as follows:

  user clicks button on webauthn page
  js sends GET request
  python reads GET request, sets up login challenge
  python returns login challenge in response
  js reads GET response, passes login challenge to browser
  browser asks user to touch yubikey
  browser returns yubikey challenge response data to js
  js sends POST request with yubikey challenge response data
  python reads yubikey challenge and compares with users creds from db
  if its a match, python signs user in

The login challenge is a PublicKeyCredentialRequestOptions: [1]
The browser function we call is navigator.credentials.get(): [2]
The response to the challenge from the browser is a PublicKeyCredential: [3]

The python server does all the work setting those up and tearing them
back down again (and checking them against the values we have stored in
the database), but we need to do work to convert them to-and-from CBOR.

[1] https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialRequestOptions
[2] https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get
[3] https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential
2021-06-01 19:08:57 +01:00

102 lines
3.4 KiB
Python

from fido2 import cbor
from fido2.client import ClientData
from fido2.ctap2 import AuthenticatorData
from flask import abort, current_app, request, session
from flask_login import current_user
from app.main import main
from app.models.user import User
from app.models.webauthn_credential import RegistrationError, WebAuthnCredential
from app.notify_client.user_api_client import user_api_client
from app.utils import redirect_to_sign_in, user_is_platform_admin
@main.route('/webauthn/register')
@user_is_platform_admin
def webauthn_begin_register():
server = current_app.webauthn_server
registration_data, state = server.register_begin(
{
"id": bytes(current_user.id, 'utf-8'),
"name": current_user.email_address,
"displayName": current_user.name,
},
credentials=[
credential.to_credential_data()
for credential in current_user.webauthn_credentials
],
user_verification="discouraged", # don't ask for PIN
authenticator_attachment="cross-platform",
)
session["webauthn_registration_state"] = state
return cbor.encode(registration_data)
@main.route('/webauthn/register', methods=['POST'])
@user_is_platform_admin
def webauthn_complete_register():
if 'webauthn_registration_state' not in session:
return cbor.encode("No registration in progress"), 400
try:
credential = WebAuthnCredential.from_registration(
session.pop("webauthn_registration_state"),
cbor.decode(request.get_data()),
)
except RegistrationError as e:
return cbor.encode(str(e)), 400
user_api_client.create_webauthn_credential_for_user(
current_user.id, credential
)
return cbor.encode('')
@main.route('/webauthn/authenticate', methods=['GET'])
@redirect_to_sign_in
def webauthn_begin_authentication():
# get user from session
user_to_login = User.from_id(session['user_details']['id'])
authentication_data, state = current_app.webauthn_server.authenticate_begin(
credentials=[
credential.to_credential_data()
for credential in user_to_login.webauthn_credentials
],
user_verification=None, # required, preferred, discouraged. sets whether to ask for PIN
)
session["webauthn_authentication_state"] = state
return cbor.encode(authentication_data)
@main.route('/webauthn/authenticate', methods=['POST'])
@redirect_to_sign_in
def webauthn_complete_authentication():
state = session.pop("webauthn_authentication_state")
request_data = cbor.decode(request.get_data())
user_id = session['user_details']['id']
user_to_login = User.from_id(user_id)
try:
current_app.webauthn_server.authenticate_complete(
state=state,
credentials=[
credential.to_credential_data()
for credential in user_to_login.webauthn_credentials
],
credential_id=request_data['credentialId'],
client_data=ClientData(request_data['clientDataJSON']),
auth_data=AuthenticatorData(request_data['authenticatorData']),
signature=request_data['signature']
)
except ValueError as exc:
current_app.logger.info(f'User {user_id} could not sign in using their webauthn token - {exc}')
abort(403)
from app.main.views.two_factor import log_in_user
return log_in_user(user_id)