1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
|
"""
flask_security.webauthn_util
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Utility class providing methods controlling various aspects of webauthn.
:copyright: (c) 2020-2024 by J. Christopher Wagner (jwag).
:license: MIT, see LICENSE for more details.
"""
from __future__ import annotations
import secrets
import typing as t
from flask import current_app, request
try:
# noinspection PyUnresolvedReferences
from webauthn.helpers.structs import (
AuthenticatorAttachment,
AuthenticatorSelectionCriteria,
ResidentKeyRequirement,
UserVerificationRequirement,
)
except ImportError: # pragma: no cover
pass
if t.TYPE_CHECKING: # pragma: no cover
import flask
from flask_security import UserMixin
class WebauthnUtil:
"""
Utility class allowing an application to fine-tune various Relying Party
attributes.
To provide your own implementation, pass in the class as ``webauthn_util_cls``
at init time. Your class will be instantiated once as part of app initialization.
.. versionadded:: 5.0.0
"""
def __init__(self, app: flask.Flask):
"""Instantiate class.
:param app: The Flask application being initialized.
"""
pass
def generate_challenge(self, nbytes: int | None = None) -> str:
# Mostly override this for testing, so we can have a 'constant' challenge.
return secrets.token_urlsafe(nbytes)
def origin(self) -> str:
# Return the RP origin - normally this is just the URL of the application.
return request.host_url.rstrip("/")
def registration_options(
self, user: UserMixin, usage: str, existing_options: dict[str, t.Any]
) -> dict[str, t.Any]:
"""
:param user: User object - could be used to configure on a per-user basis.
:param usage: Either "first" or "secondary" (webauthn is being used as a second
factor for authentication)
:param existing_options: Currently filled in registration options.
Return a dict that will be sent in to py-webauthn generate_registration_options
"""
existing_options["authenticator_selection"] = self.authenticator_selection(
user, usage
)
return existing_options
def authenticator_selection(
self, user: UserMixin, usage: str
) -> AuthenticatorSelectionCriteria:
"""
:param user: User object - could be used to configure on a per-user basis.
:param usage: Either "first" or "secondary" (webauthn is being used as a second
factor for authentication
Part of the registration ceremony is providing information about what kind
of authenticators the app is interested in.
See: https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#dictionary-authenticatorSelection
The main options are:
- whether you want a ResidentKey (discoverable)
- Attachment - platform or cross-platform
- Does the key have to provide user-verification
:note::
If the key isn't resident then it isn't discoverable which means that
the user won't be able to use that key unless they identify themselves
(use the key as a second factor OR type in their identity). If they are forced
to type in their identity PRIOR to being authenticated, then there is the
possibility that the app will leak username information.
""" # noqa: E501
select_criteria = AuthenticatorSelectionCriteria()
# TODO: look at #sctn-usecase-new-device-registration to see a reason
# to allow multiple keys as "first" - only one would need to be cross-platform
if usage == "first":
select_criteria.authenticator_attachment = (
AuthenticatorAttachment.CROSS_PLATFORM
)
select_criteria.user_verification = UserVerificationRequirement.PREFERRED
else:
# For second factor minimize user-interaction by not asking for UV
select_criteria.user_verification = UserVerificationRequirement.DISCOURAGED
if not current_app.config.get("SECURITY_WAN_ALLOW_USER_HINTS"):
select_criteria.resident_key = ResidentKeyRequirement.REQUIRED
else:
select_criteria.resident_key = ResidentKeyRequirement.PREFERRED
return select_criteria
def authentication_options(
self,
user: UserMixin | None,
usage: list[str],
existing_options: dict[str, t.Any],
) -> dict[str, t.Any]:
"""
:param user: User object - could be used to configure on a per-user basis.
However, this can be null.
:param usage: Either "first" or "secondary" (webauthn is being used as a second
factor for authentication)
:param existing_options: Currently filled in authentication options.
Return a dict that will be sent in to
py-webauthn generate_authentication_options
"""
existing_options["user_verification"] = self.user_verification(user, usage)
return existing_options
def user_verification(
self, user: UserMixin | None, usage: list[str]
) -> UserVerificationRequirement:
"""
As part of signin - do we want/need user verification.
This is called from /wan-signin and /wan-verify
:param user: User object - could be used to configure on a per-user basis.
Note that this may not be set on initial wan-signin.
:param usage: List of "first", "secondary" (webauthn is being used as a second
factor for authentication). Note that in the ``verify``/``reauthentication``
case this list is derived from :py:data:`SECURITY_WAN_ALLOW_AS_VERIFY`
"""
if "secondary" in usage:
return UserVerificationRequirement.DISCOURAGED
if current_app.config.get("SECURITY_WAN_ALLOW_AS_MULTI_FACTOR"):
return UserVerificationRequirement.PREFERRED
return UserVerificationRequirement.PREFERRED
|