Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
recovery_codes.py250 linesDownload Raw Back to flask_security
1"""2    flask_security.recovery_codes3    ~~~~~~~~~~~~~~~~~~~~~~~~4 5    Flask-Security Recovery Codes Module6 7    :copyright: (c) 2022-2024 by J. Christopher Wagner (jwag).8    :license: MIT, see LICENSE for more details.9"""10 11from __future__ import annotations12 13import typing as t14 15from flask import after_this_request, request, redirect16from flask_login import current_user17 18 19from .decorators import anonymous_user_required, auth_required, unauth_csrf20from .forms import (21    build_form_from_request,22    get_form_field_label,23    get_form_field_xlate,24    Form,25    Required,26    StringField,27    SubmitField,28)29from .proxies import _datastore, _security30from .tf_plugin import tf_check_state, tf_illegal_state31from .utils import (32    _,33    base_render_json,34    config_value as cv,35    get_message,36    get_post_login_redirect,37    view_commit,38)39 40if t.TYPE_CHECKING:  # pragma: no cover41    from cryptography.fernet import MultiFernet42    import flask43    from flask.typing import ResponseValue44    from .datastore import User45 46 47class MfRecoveryCodesUtil:48    """Handle creation, checking, encrypting and decrypting recovery codes.49    Since these are rarely used - keep them encrypted until needed - yes50    if someone gets access to memory they can find the key...51    """52 53    def __init__(self, app: flask.Flask):54        self.cryptor: MultiFernet | None = None55        keys = cv("MULTI_FACTOR_RECOVERY_CODES_KEYS", app)56        # N.B. order is important - first key is 'primary'.57        if keys:58            self.setup_cryptor(keys)59 60    def setup_cryptor(self, keys: list[bytes]) -> None:61        from cryptography.fernet import Fernet, MultiFernet62 63        cryptors: list[Fernet] = []64        for key in keys:65            cryptors.append(Fernet(key))66        self.cryptor = MultiFernet(cryptors)67 68    def create_recovery_codes(self, user: User) -> list[str]:69        # Create new recovery codes and store in user record.70        # If configured codes are stored encrypted - but plainttext71        # versions are returned.72        new_codes = _security._totp_factory.generate_recovery_codes(73            cv("MULTI_FACTOR_RECOVERY_CODES_N")74        )75        _datastore.mf_set_recovery_codes(user, self.encrypt_codes(new_codes))76        return new_codes77 78    def get_recovery_codes(self, user: User) -> list[str]:79        ecodes = _datastore.mf_get_recovery_codes(user)80        return self.decrypt_codes(ecodes)81 82    def check_recovery_code(self, user: User, code: str) -> bool:83        # Verify code is valid84        codes = _datastore.mf_get_recovery_codes(user)85        dcodes = self.decrypt_codes(codes)86        return code in dcodes87 88    def delete_recovery_code(self, user: User, code: str) -> bool:89        # codes are single use - so delete after use.90        # encrypting code gives different answer due to time stamp.91        # we don't want to re-encrypt other codes.92        codes = _datastore.mf_get_recovery_codes(user)93        if self.cryptor:94            codes = self.decrypt_codes(codes)95        idx = codes.index(code)96        return _datastore.mf_delete_recovery_code(user, idx)97 98    def encrypt_codes(self, codes: list[str]) -> list[str]:99        if not self.cryptor:100            return codes101        ecodes = []102        for code in codes:103            ecodes.append(self.cryptor.encrypt(code.encode()).decode())104        return ecodes105 106    def decrypt_codes(self, codes: list[str]) -> list[str]:107        from cryptography.fernet import InvalidToken108 109        if not self.cryptor:110            return codes111        dcodes = []112        for code in codes:113            try:114                dcode = self.cryptor.decrypt(115                    code.encode(), cv("MULTI_FACTOR_RECOVERY_CODE_TTL")116                )117                dcodes.append(dcode.decode())118            except InvalidToken:119                # should we delete this?120                pass121        return dcodes122 123 124class MfRecoveryCodesForm(Form):125    """Generate and fetch recovery codes"""126 127    # show_codes is a GET option., generate_new_codes is a POST option128    show_codes = SubmitField(get_form_field_xlate(_("Show Recovery Codes")))129    generate_new_codes = SubmitField(130        get_form_field_xlate(_("Generate New Recovery Codes"))131    )132 133    def __init__(self, *args, **kwargs):134        super().__init__(*args, **kwargs)135 136    def validate(self, **kwargs: t.Any) -> bool:137        if not super().validate(**kwargs):  # pragma: no cover138            return False139        return True140 141 142class MfRecoveryForm(Form):143    """Accept recovery code for second factor authentication"""144 145    code = StringField(146        get_form_field_xlate(_("Recovery Code")),147        validators=[Required()],148    )149    submit = SubmitField(get_form_field_label("submitcode"))150 151    def __init__(self, *args: t.Any, **kwargs: t.Any):152        super().__init__(*args, **kwargs)153        # filled by view154        self.user: User | None = None155 156    def validate(self, **kwargs: t.Any) -> bool:157        if not super().validate(**kwargs):  # pragma: no cover158            return False159        assert self.user is not None160        if not _security._mf_recovery_codes_util.check_recovery_code(161            self.user, self.code.data162        ):163            self.code.errors.append(get_message("INVALID_RECOVERY_CODE")[0])164            return False165        return True166 167 168@auth_required(169    lambda: cv("API_ENABLED_METHODS"),170    within=lambda: cv("FRESHNESS"),171    grace=lambda: cv("FRESHNESS_GRACE_PERIOD"),172)173def mf_recovery_codes() -> ResponseValue:174    """175    Create and download multi-factor recovery codes.176    For forms, we want the user to explicitly request to see the codes - so177    the form has a show_codes submit button.178    """179    form = t.cast(180        MfRecoveryCodesForm, build_form_from_request("mf_recovery_codes_form")181    )182 183    if form.validate_on_submit():184        # generate new codes185        codes = _security._mf_recovery_codes_util.create_recovery_codes(current_user)186        after_this_request(view_commit)187        if _security._want_json(request):188            payload = dict(recovery_codes=codes)189            return base_render_json(form, include_user=False, additional=payload)190        return _security.render_template(191            cv("MULTI_FACTOR_RECOVERY_CODES_TEMPLATE"),192            mf_recovery_codes_form=form,193            recovery_codes=codes,194            **_security._run_ctx_processor("mf_recovery_codes"),195        )196 197    codes = _security._mf_recovery_codes_util.get_recovery_codes(current_user)198    if _security._want_json(request):199        return base_render_json(200            form, include_user=False, additional=dict(recovery_codes=codes)201        )202    show_codes = request.args.get("show_codes", False)203    if show_codes and not codes:204        form.show_codes.errors = []205        form.show_codes.errors.append(get_message("NO_RECOVERY_CODES_SETUP")[0])206    return _security.render_template(207        cv("MULTI_FACTOR_RECOVERY_CODES_TEMPLATE"),208        mf_recovery_codes_form=form,209        recovery_codes=codes if show_codes else [],210        **_security._run_ctx_processor("mf_recovery_codes"),211    )212 213 214@anonymous_user_required215@unauth_csrf()216def mf_recovery():217    """View for entering a recovery code.218 219    User must have already provided valid username/password.220    User must have already established 2FA221 222    """223    form = t.cast(MfRecoveryForm, build_form_from_request("mf_recovery_form"))224    form.user = tf_check_state(["ready"])225    if not form.user:226        return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW"))227 228    if form.validate_on_submit():229        # Valid code - we want these to be one time - so remove it from list230        _security._mf_recovery_codes_util.delete_recovery_code(231            form.user, form.code.data232        )233        after_this_request(view_commit)234 235        # In the recovery case - don't set/offer validity token.236        _security.two_factor_plugins.tf_complete(form.user, True)237 238        if not _security._want_json(request):239            return redirect(get_post_login_redirect())240        else:241            return base_render_json(form)242 243    if _security._want_json(request):244        return base_render_json(form, include_user=False)245    return _security.render_template(246        cv("MULTI_FACTOR_RECOVERY_TEMPLATE"),247        mf_recovery_form=form,248        **_security._run_ctx_processor("mf_recovery"),249    )250 
codekingpro/portable-devtools · Team Ai