File: webauthn_util.py

package info (click to toggle)
flask-security 5.6.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 3,448 kB
  • sloc: python: 23,247; javascript: 204; makefile: 138
file content (158 lines) | stat: -rw-r--r-- 6,115 bytes parent folder | download | duplicates (2)
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