Join GitHub today
GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.
Sign up| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| import base64 | |
| import os | |
| import webauthn as pywebauthn | |
| from webauthn.webauthn import ( | |
| AuthenticationRejectedException as _AuthenticationRejectedException, | |
| RegistrationRejectedException as _RegistrationRejectedException, | |
| ) | |
| class AuthenticationRejectedException(Exception): | |
| pass | |
| class RegistrationRejectedException(Exception): | |
| pass | |
| WebAuthnCredential = pywebauthn.WebAuthnCredential | |
| def _get_webauthn_users(user, *, rp_id): | |
| """ | |
| Returns a webauthn.WebAuthnUser instance corresponding | |
| to the given user model, with properties suitable for | |
| usage within the webauthn API. | |
| """ | |
| return [ | |
| pywebauthn.WebAuthnUser( | |
| str(user.id), | |
| user.username, | |
| user.name, | |
| None, | |
| credential.credential_id, | |
| credential.public_key, | |
| credential.sign_count, | |
| rp_id, | |
| ) | |
| for credential in user.webauthn | |
| ] | |
| def _webauthn_b64decode(encoded): | |
| padding = "=" * (len(encoded) % 4) | |
| return base64.urlsafe_b64decode(encoded + padding) | |
| def _webauthn_b64encode(source): | |
| return base64.urlsafe_b64encode(source).rstrip(b"=") | |
| def generate_webauthn_challenge(): | |
| """ | |
| Returns a random challenge suitable for use within | |
| Webauthn's credential and configuration option objects. | |
| See: https://w3c.github.io/webauthn/#cryptographic-challenges | |
| """ | |
| # NOTE: Webauthn recommends at least 16 bytes of entropy, | |
| # we go with 32 because it doesn't cost us anything. | |
| return _webauthn_b64encode(os.urandom(32)).decode() | |
| def get_credential_options(user, *, challenge, rp_name, rp_id): | |
| """ | |
| Returns a dictionary of options for credential creation | |
| on the client side. | |
| """ | |
| options = pywebauthn.WebAuthnMakeCredentialOptions( | |
| challenge, | |
| rp_name, | |
| rp_id, | |
| str(user.id), | |
| user.username, | |
| user.name, | |
| None, | |
| user_verification="discouraged", | |
| ) | |
| return options.registration_dict | |
| def get_assertion_options(user, *, challenge, rp_id): | |
| """ | |
| Returns a dictionary of options for assertion retrieval | |
| on the client side. | |
| """ | |
| options = pywebauthn.WebAuthnAssertionOptions( | |
| _get_webauthn_users(user, rp_id=rp_id), challenge | |
| ) | |
| return options.assertion_dict | |
| def verify_registration_response(response, challenge, *, rp_id, origin): | |
| """ | |
| Validates the challenge and attestation information | |
| sent from the client during device registration. | |
| Returns a WebAuthnCredential on success. | |
| Raises RegistrationRejectedException on failire. | |
| """ | |
| # NOTE: We re-encode the challenge below, because our | |
| # response's clientData.challenge is encoded twice: | |
| # first for the entire clientData payload, and then again | |
| # for the individual challenge. | |
| encoded_challenge = _webauthn_b64encode(challenge.encode()).decode() | |
| response = pywebauthn.WebAuthnRegistrationResponse( | |
| rp_id, origin, response, encoded_challenge, self_attestation_permitted=True | |
| ) | |
| try: | |
| return response.verify() | |
| except _RegistrationRejectedException as e: | |
| raise RegistrationRejectedException(str(e)) | |
| def verify_assertion_response(assertion, *, challenge, user, origin, rp_id): | |
| """ | |
| Validates the challenge and assertion information | |
| sent from the client during authentication. | |
| Returns an updated signage count on success. | |
| Raises AuthenticationRejectedException on failure. | |
| """ | |
| webauthn_users = _get_webauthn_users(user, rp_id=rp_id) | |
| cred_ids = [cred.credential_id for cred in webauthn_users] | |
| encoded_challenge = _webauthn_b64encode(challenge.encode()).decode() | |
| for webauthn_user in webauthn_users: | |
| response = pywebauthn.WebAuthnAssertionResponse( | |
| webauthn_user, | |
| assertion, | |
| encoded_challenge, | |
| origin, | |
| allow_credentials=cred_ids, | |
| ) | |
| try: | |
| return (webauthn_user.credential_id, response.verify()) | |
| except _AuthenticationRejectedException: | |
| pass | |
| # If we exit the loop, then we've failed to verify the assertion against | |
| # any of the user's WebAuthn credentials. Fail. | |
| raise AuthenticationRejectedException("Invalid WebAuthn credential") |