Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
tf_plugin.py355 linesDownload Raw Back to flask_security
1"""2    flask_security.tf_plugin3    ~~~~~~~~~~~~~~~~~~~~~~~~4 5    Flask-Security Two-Factor Plugin Module6 7    :copyright: (c) 2022-2024 by J. Christopher Wagner (jwag).8    :license: MIT, see LICENSE for more details.9 10    TODO:11        - add localized callback for select choices.12"""13 14from __future__ import annotations15 16import typing as t17 18from flask import request, redirect, session19 20from .decorators import unauth_csrf21from .forms import (22    build_form_from_request,23    get_form_field_xlate,24    Form,25    RadioField,26    SubmitField,27)28from .proxies import _datastore, _security29from .utils import (30    _,31    base_render_json,32    check_and_get_token_status,33    config_value as cv,34    do_flash,35    get_message,36    get_within_delta,37    get_url,38    login_user,39    propagate_next,40    simple_render_json,41    url_for_security,42)43 44if t.TYPE_CHECKING:  # pragma: no cover45    import flask46    from flask.typing import ResponseValue47    from flask import Response48    from .core import Security49    from .datastore import User50 51 52class TwoFactorSelectForm(Form):53    which = RadioField(get_form_field_xlate(_("Available Second Factor Methods:")))54    submit = SubmitField(get_form_field_xlate(_("Select")))55 56    def __init__(self, *args, **kwargs):57        super().__init__(*args, **kwargs)58 59 60@unauth_csrf()61def tf_select() -> ResponseValue:62    # Ask user which MFA method they want to use.63    # This is used when a user has setup more than one type of 2FA.64    form = t.cast(65        TwoFactorSelectForm, build_form_from_request("two_factor_select_form")66    )67 68    # This endpoint is unauthenticated - make sure we're in a valid state69    if not all(k in session for k in ["tf_user_id", "tf_select"]):70        # illegal call on this endpoint71        tf_clean_session()72        return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW"))73 74    user = _datastore.find_user(fs_uniquifier=session["tf_user_id"])75    if not user:  # pragma no cover76        # hard to imagine - someone deletes the user while they are logging in.77        tf_clean_session()78        return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW"))79 80    setup_methods = _security.two_factor_plugins.get_setup_tf_methods(user)81    form.which.choices = setup_methods82 83    if form.validate_on_submit():84        response = None85        tf_impl = _security.two_factor_plugins.method_to_impl(user, form.which.data)86        if tf_impl:87            json_payload = {"tf_required": True}88            response = tf_impl.tf_login(89                user, json_payload, next_loc=propagate_next(request.url, None)90            )91        if not response:  # pragma no cover92            # This really can't happen unless between the time the started logging in93            # and now, they deleted a second factor (which they would have to do94            # in another window).95            tf_clean_session()96            return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW"))97        return response98 99    if _security._want_json(request):100        payload = {"tf_select": True, "tf_setup_methods": setup_methods}101        return base_render_json(form, include_user=False, additional=payload)102 103    return _security.render_template(104        cv("TWO_FACTOR_SELECT_TEMPLATE"),105        two_factor_select_form=form,106        **_security._run_ctx_processor("tf_select"),107    )108 109 110class TfPluginBase:  # pragma no cover111    def __init__(self, app: flask.Flask):112        pass113 114    def create_blueprint(115        self, app: flask.Flask, bp: flask.Blueprint, state: Security116    ) -> None:117        raise NotImplementedError118 119    def get_setup_methods(self, user: User) -> list[str]:120        """121        Return a list of methods that ``user`` has setup for this second factor122        """123        raise NotImplementedError124 125    def tf_login(126        self, user: User, json_payload: dict[str, t.Any], next_loc: str | None127    ) -> ResponseValue:128        """129        Called from first/primary authenticated views if the user successfully130        authenticated, and required a second method of authentication.131        This method returns the necessary information for the user UI to continue.132        For forms, this is usually a redirect to a secondary sign in form. For JSON133        it is just a payload that describes what the user has to do next.134        """135        raise NotImplementedError136 137 138class TfPlugin:139    """140    Two-Factor plugin support.141 142    Enables multiple independent two-factor implementations to be configured for a given143    app. See TfPluginBase for what a new implementation must provide.144    """145 146    def __init__(self) -> None:147        self._tf_impls: dict[str, TfPluginBase] = {}148 149    def register_tf_impl(150        # N.B. all methods must be unique across all implementations.151        self,152        app: flask.Flask,153        name: str,154        impl: t.Type[TfPluginBase],155    ) -> None:156        self._tf_impls[name] = impl(app)157 158    def create_blueprint(159        self, app: flask.Flask, bp: flask.Blueprint, state: Security160    ) -> None:161        if state.support_mfa:162            for impl in self._tf_impls.values():163                impl.create_blueprint(app, bp, state)164            # Add our route for selecting between multiple active two-factor165            # mechanisms.166            bp.route(167                cv("TWO_FACTOR_SELECT_URL", app),168                methods=["GET", "POST"],169                endpoint="tf_select",170            )(tf_select)171 172    def method_to_impl(self, user: User, method: str) -> TfPluginBase | None:173        # reverse map a method to the implementation.174        # N.B. again - requires that methods be unique across all implementations.175        # There is a small window that a previously setup method was removed.176        for impl in self._tf_impls.values():177            setup_methods = impl.get_setup_methods(user)178            if method in setup_methods:179                return impl180        return None  # pragma no cover181 182    def get_setup_tf_methods(self, user: User) -> list[str]:183        # Return list of methods that user has setup184        methods = []185        for impl in self._tf_impls.values():186            methods.extend(impl.get_setup_methods(user))187        return methods188 189    def tf_enter(190        self,191        user: User,192        remember_me: bool,193        primary_authn_via: str,194        next_loc: str | None,195    ) -> ResponseValue | None:196        """Check if two-factor is required and if so, start the process.197        Must be called in a request context.198        remember_me controls 2 cookies - the remember_me cookie and the tf_validity199        cookie. We use the session to hold the fact that the user requested 'remember'200        across the second factor.201        """202        json_payload: dict[str, t.Any]203        if _security.support_mfa:204            tf_setup_methods = self.get_setup_tf_methods(user)205            if cv("TWO_FACTOR_REQUIRED") or len(tf_setup_methods) > 0:206                tf_fresh = tf_verify_validity_token(user.fs_uniquifier)207                if cv("TWO_FACTOR_ALWAYS_VALIDATE") or not tf_fresh:208                    # Clean out any potential old session info - in case of previous209                    # aborted 2FA attempt.210                    tf_clean_session()211 212                    json_payload = {"tf_required": True}213                    if remember_me:214                        session["tf_remember_login"] = remember_me215 216                    session["tf_user_id"] = user.fs_uniquifier217                    # A backwards compat hack - the original twofactor could be setup218                    # as part of initial login.219                    if len(tf_setup_methods) == 0:220                        # only initial two-factor implementation supports this221                        return self._tf_impls["code"].tf_login(222                            user, json_payload, next_loc223                        )224                    elif len(tf_setup_methods) == 1:225                        # method_to_impl can't return None here since we just226                        # got the methods up above.227                        impl = t.cast(228                            TfPluginBase,229                            self.method_to_impl(user, tf_setup_methods[0]),230                        )231                        return impl.tf_login(user, json_payload, next_loc)232                    else:233                        session["tf_select"] = True234                        if not _security._want_json(request):235                            values = dict(next=next_loc) if next_loc else dict()236                            return redirect(url_for_security("tf_select", **values))237                        # Let's force app to go through tf-select just in case we want238                        # to do further validation... However, provide the choices239                        # so they can just do a POST240                        json_payload.update(241                            {242                                "tf_select": True,243                                "tf_setup_methods": tf_setup_methods,244                            }245                        )246                        return simple_render_json(json_payload)247        return None248 249    def tf_complete(self, user: User, dologin: bool) -> str | None:250        remember = session.pop("tf_remember_login", None)251 252        if dologin:253            login_user(user, remember=remember)254        tf_clean_session()255        token = None256        # return a token to avoid future two-factor prompts (for a period of time)257        if not cv("TWO_FACTOR_ALWAYS_VALIDATE") and remember:258            token = generate_tf_validity_token(user.fs_uniquifier)259        return token260 261 262def generate_tf_validity_token(fs_uniquifier):263    """Generates a unique token for the specified user.264 265    :param fs_uniquifier: The fs_uniquifier of a user to whom the token belongs to266    """267    return _security.tf_validity_serializer.dumps(fs_uniquifier)268 269 270def tf_validity_token_status(token):271    """Returns the expired status, invalid status, and user of a272    Two-Factor Validity token.273    For example::274 275        expired, invalid, user = tf_validity_token_status('...')276 277    :param token: The Two-Factor Validity token278    """279    return check_and_get_token_status(280        token, "tf_validity", get_within_delta("TWO_FACTOR_LOGIN_VALIDITY")281    )282 283 284def tf_verify_validity_token(fs_uniquifier: str) -> bool:285    """Returns the status of the Two-Factor Validity token based on the current286    request.287 288    :param fs_uniquifier: The ``fs_uniquifier`` of the submitting user.289    """290    token = request.cookies.get("tf_validity", default=None)291    if token is None:292        return False293 294    expired, invalid, uniquifier = tf_validity_token_status(token)295    if expired or invalid or (fs_uniquifier != uniquifier):296        return False297 298    return True299 300 301def tf_set_validity_token_cookie(response: Response, token: str) -> Response:302    """Sets the Two-Factor validity token for a specific user given that is303    configured and the user selects remember me304 305    :param response: The response with which to set the set_cookie306    :param token: validity token307    """308    cookie_kwargs = cv("TWO_FACTOR_VALIDITY_COOKIE")309    max_age = int(get_within_delta("TWO_FACTOR_LOGIN_VALIDITY").total_seconds())310    response.set_cookie("tf_validity", value=token, max_age=max_age, **cookie_kwargs)311    # This is likely overkill since so far we only return this on a POST which is312    # unlikely to be cached.313    response.vary.add("Cookie")314    return response315 316 317def tf_check_state(allowed_states: list[str]) -> User | None:318    if (319        not all(k in session for k in ["tf_user_id", "tf_state"])320        or session["tf_state"] not in allowed_states321    ):322        tf_clean_session()323        return None324 325    user = _datastore.find_user(fs_uniquifier=session["tf_user_id"])326    if not user:327        tf_clean_session()328    return user329 330 331def tf_illegal_state(form, redirect_to):332    m, c = get_message("TWO_FACTOR_PERMISSION_DENIED")333    if not _security._want_json(request):334        do_flash(m, c)335        return redirect(get_url(redirect_to))336    else:337        form.form_errors.append(m)338        return base_render_json(form, include_user=False)339 340 341def tf_clean_session():342    """343    Clean out ALL stuff stored in session (e.g. on logout or restart of a session)344    """345    if cv("TWO_FACTOR"):346        for k in [347            "tf_state",348            "tf_user_id",349            "tf_primary_method",350            "tf_remember_login",351            "tf_totp_secret",352            "tf_select",353        ]:354            session.pop(k, None)355