Beispiel #1
0
    def authenticate(self, username, password):
        """
        Verifies that a password matches the stored password for that username.
        If verification passes, the matching user instance is returned
        """
        PraetorianError.require_condition(
            self.user_class is not None,
            "Praetorian must be initialized before this method is available",
        )
        user = self.user_class.lookup(username)
        MissingUserError.require_condition(
            user is not None,
            'Could not find the requested user',
        )
        AuthenticationError.require_condition(
            self._verify_password(password, user.password),
            'The password is incorrect',
        )
        """
        If we are set to PRAETORIAN_HASH_AUTOUPDATE then check our hash
            and if needed, update the user.  The developer is responsible
            for using the returned user object and updating the data
            storage endpoint.
        Else, if we are set to PRAETORIAN_HASH_AUTOTEST then check out hash
            and return exception if our hash is using the wrong scheme,
            but don't modify the user.
        """
        if self.hash_autoupdate:
            self.verify_and_update(user=user, password=password)
        elif self.hash_autotest:
            self.verify_and_update(user=user)

        return user
Beispiel #2
0
 def _verify_password(self, raw_password, hashed_password):
     """
     Verifies that a plaintext password matches the hashed version of that
     password using the stored passlib password context
     """
     PraetorianError.require_condition(
         self.pwd_ctx is not None,
         "Praetorian must be initialized before this method is available",
     )
     return self.pwd_ctx.verify(raw_password, hashed_password)
Beispiel #3
0
def current_guard():
    """
    Fetches the current instance of flask-praetorian that is attached to the
    current flask app
    """
    guard = flask.current_app.extensions.get('praetorian', None)
    PraetorianError.require_condition(
        guard is not None,
        "No current guard found; Praetorian must be initialized first",
    )
    return guard
Beispiel #4
0
def current_user_id():
    """
    This method returns the user id retrieved from jwt token data attached to
    the current flask app's context
    """
    jwt_data = get_jwt_data_from_app_context()
    user_id = jwt_data.get('id')
    PraetorianError.require_condition(
        user_id is not None,
        "Could not fetch an id for the current user",
    )
    return user_id
Beispiel #5
0
def current_user():
    """
    This method returns a user instance for jwt token data attached to the
    current flask app's context
    """
    user_id = current_user_id()
    guard = current_guard()
    user = guard.user_class.identify(user_id)
    PraetorianError.require_condition(
        user is not None,
        "Could not identify the current user from the current id",
    )
    return user
Beispiel #6
0
def get_jwt_data_from_app_context():
    """
    Fetches a dict of jwt token data from the top of the flask app's context
    """
    ctx = flask._app_ctx_stack.top
    jwt_data = getattr(ctx, 'jwt_data', None)
    PraetorianError.require_condition(
        jwt_data is not None,
        """
        No jwt_data found in app context.
        Make sure @auth_required decorator is specified *first* for route
        """,
    )
    return jwt_data
Beispiel #7
0
 def hash_password(self, raw_password):
     """
     Hashes a plaintext password using the stored passlib password context
     """
     PraetorianError.require_condition(
         self.pwd_ctx is not None,
         "Praetorian must be initialized before this method is available",
     )
     """
     `scheme` is now set with self.pwd_ctx.update(default=scheme) due
         to the depreciation in upcoming passlib 2.0.
      zillions of warnings suck.
     """
     return self.pwd_ctx.hash(raw_password)
Beispiel #8
0
 def validate_reset_token(self, token):
     """
     Validates a password reset request based on the reset token
     that is supplied. Verifies that the token is a reset token
     and that the user can be properly retrieved
     """
     data = self.extract_jwt_token(token, access_type=AccessType.reset)
     user_id = data.get('id')
     PraetorianError.require_condition(
         user_id is not None,
         "Could not fetch an id from the reset token",
     )
     user = self.user_class.identify(user_id)
     PraetorianError.require_condition(
         user is not None,
         "Could not identify the user from the reset token",
     )
     return user
Beispiel #9
0
 def get_user_from_registration_token(self, token):
     """
     Gets a user based on the registration token that is supplied. Verifies
     that the token is a regisration token and that the user can be properly
     retrieved
     """
     data = self.extract_jwt_token(token, access_type=AccessType.register)
     flask.current_app.logger.debug("DATA: {}".format(data))
     user_id = data.get('id')
     PraetorianError.require_condition(
         user_id is not None,
         "Could not fetch an id from the registration token",
     )
     user = self.user_class.identify(user_id)
     PraetorianError.require_condition(
         user is not None,
         "Could not identify the user from the registration token",
     )
     return user
Beispiel #10
0
 def _validate_user_class(cls, user_class):
     """
     Validates the supplied user_class to make sure that it has the
     class methods necessary to function correctly.
     Requirements:
     - ``lookup`` method. Accepts a string parameter, returns instance
     - ``identify`` method. Accepts an identity parameter, returns instance
     """
     PraetorianError.require_condition(
         getattr(user_class, 'lookup', None) is not None,
         textwrap.dedent("""
             The user_class must have a lookup class method:
             user_class.lookup(<str>) -> <user instance>
         """),
     )
     PraetorianError.require_condition(
         getattr(user_class, 'identify', None) is not None,
         textwrap.dedent("""
             The user_class must have an identify class method:
             user_class.identify(<identity>) -> <user instance>
         """),
     )
     # TODO: Figure out how to check for an identity property
     return user_class
Beispiel #11
0
    def init_app(
        self,
        app=None,
        user_class=None,
        is_blacklisted=None,
        encode_jwt_token_hook=None,
        refresh_jwt_token_hook=None,
    ):
        """
        Initializes the Praetorian extension
        :param: app:                    The flask app to bind this
                                        extension to
        :param: user_class:             The class used to interact with
                                        user data
        :param: is_blacklisted:         A method that may optionally be
                                        used to check the token against
                                        a blacklist when access or refresh
                                        is requested should take the jti
                                        for the token to check as a single
                                        argument. Returns True if the jti is
                                        blacklisted, False otherwise. By
                                        default, always returns False.
        :param encode_jwt_token_hook:   A method that may optionally be
                                        called right before an encoded jwt
                                        is generated. Should take
                                        payload_parts which contains the
                                        ingredients for the jwt.
        :param refresh_jwt_token_hook:  A method that may optionally be called
                                        right before an encoded jwt is
                                        refreshed. Should take payload_parts
                                        which contains the ingredients for
                                        the jwt.
        """
        PraetorianError.require_condition(
            app.config.get('SECRET_KEY') is not None,
            "There must be a SECRET_KEY app config setting set",
        )

        self.hash_autoupdate = app.config.get(
            'PRAETORIAN_HASH_AUTOUPDATE',
            DEFAULT_HASH_AUTOUPDATE,
        )

        self.hash_autotest = app.config.get(
            'PRAETORIAN_HASH_AUTOTEST',
            DEFAULT_HASH_AUTOTEST,
        )

        self.pwd_ctx = CryptContext(
            schemes=app.config.get('PRAETORIAN_HASH_ALLOWED_SCHEMES',
                                   DEFAULT_HASH_ALLOWED_SCHEMES),
            default=app.config.get('PRAETORIAN_HASH_SCHEME',
                                   DEFAULT_HASH_SCHEME),
            deprecated=app.config.get('PRAETORIAN_HASH_DEPRECATED_SCHEMES',
                                      DEFAULT_HASH_DEPRECATED_SCHEMES),
        )

        valid_schemes = self.pwd_ctx.schemes()
        PraetorianError.require_condition(
            self.hash_scheme in valid_schemes or self.hash_scheme is None,
            "If {} is set, it must be one of the following schemes: {}",
            'PRAETORIAN_HASH_SCHEME',
            valid_schemes,
        )

        self.user_class = self._validate_user_class(user_class)
        self.is_blacklisted = is_blacklisted or (lambda t: False)
        self.encode_jwt_token_hook = encode_jwt_token_hook
        self.refresh_jwt_token_hook = refresh_jwt_token_hook

        self.encode_key = app.config['SECRET_KEY']
        self.allowed_algorithms = app.config.get(
            'JWT_ALLOWED_ALGORITHMS',
            DEFAULT_JWT_ALLOWED_ALGORITHMS,
        )
        self.encode_algorithm = app.config.get(
            'JWT_ALGORITHM',
            DEFAULT_JWT_ALGORITHM,
        )
        self.access_lifespan = app.config.get(
            'JWT_ACCESS_LIFESPAN',
            DEFAULT_JWT_ACCESS_LIFESPAN,
        )
        self.refresh_lifespan = app.config.get(
            'JWT_REFRESH_LIFESPAN',
            DEFAULT_JWT_REFRESH_LIFESPAN,
        )
        self.reset_lifespan = app.config.get(
            'JWT_RESET_LIFESPAN',
            DEFAULT_JWT_RESET_LIFESPAN,
        )
        self.header_name = app.config.get(
            'JWT_HEADER_NAME',
            DEFAULT_JWT_HEADER_NAME,
        )
        self.header_type = app.config.get(
            'JWT_HEADER_TYPE',
            DEFAULT_JWT_HEADER_TYPE,
        )
        self.user_class_validation_method = app.config.get(
            'USER_CLASS_VALIDATION_METHOD',
            DEFAULT_USER_CLASS_VALIDATION_METHOD,
        )

        self.confirmation_template = app.config.get(
            'PRAETORIAN_CONFIRMATION_TEMPLATE',
            DEFAULT_CONFIRMATION_TEMPLATE,
        )
        self.confirmation_uri = app.config.get('PRAETORIAN_CONFIRMATION_URI', )
        self.confirmation_sender = app.config.get(
            'PRAETORIAN_CONFIRMATION_SENDER', )
        self.confirmation_subject = app.config.get(
            'PRAETORIAN_CONFIRMATION_SUBJECT',
            DEFAULT_CONFIRMATION_SUBJECT,
        )

        self.reset_template = app.config.get(
            'PRAETORIAN_RESET_TEMPLATE',
            DEFAULT_RESET_TEMPLATE,
        )
        self.reset_uri = app.config.get('PRAETORIAN_RESET_URI', )
        self.reset_sender = app.config.get('PRAETORIAN_RESET_SENDER', )
        self.reset_subject = app.config.get(
            'PRAETORIAN_RESET_SUBJECT',
            DEFAULT_RESET_SUBJECT,
        )

        if isinstance(self.access_lifespan, dict):
            self.access_lifespan = pendulum.duration(**self.access_lifespan)
        elif isinstance(self.access_lifespan, str):
            self.access_lifespan = duration_from_string(self.access_lifespan)
        ConfigurationError.require_condition(
            isinstance(self.access_lifespan, datetime.timedelta),
            "access lifespan was not configured",
        )

        if isinstance(self.refresh_lifespan, dict):
            self.refresh_lifespan = pendulum.duration(**self.refresh_lifespan)
        if isinstance(self.refresh_lifespan, str):
            self.refresh_lifespan = duration_from_string(self.refresh_lifespan)
        ConfigurationError.require_condition(
            isinstance(self.refresh_lifespan, datetime.timedelta),
            "refresh lifespan was not configured",
        )

        if not app.config.get('DISABLE_PRAETORIAN_ERROR_HANDLER'):
            app.register_error_handler(
                PraetorianError,
                PraetorianError.build_error_handler(),
            )

        self.is_testing = app.config.get('TESTING', False)

        if not hasattr(app, 'extensions'):
            app.extensions = {}
        app.extensions['praetorian'] = self

        return app
Beispiel #12
0
    def send_token_email(self,
                         email,
                         user=None,
                         template=None,
                         action_sender=None,
                         action_uri=None,
                         subject=None,
                         override_access_lifespan=None,
                         custom_token=None,
                         sender='no-reply@praetorian'):
        """
        Sends an email to a user, containing a time expiring
            token usable for several actions.  This requires
            your application is initialized with a `mail` extension,
            which supports Flask-Mail's `Message()` object and
            a `send()` method.
        Returns a dict containing the information sent, along with the
            `result` from mail send.
        :param: email:                    The email address to use
                                          (username, id, email, etc)
        :param: user:                     The user object to tie claim to
                                          (username, id, email, etc)
        :param: template:                 HTML Template for confirmation email.
                                          If not provided, a stock entry is
                                          used
        :param: action_sender:            The sender that should be attached
                                          to the confirmation email.
        :param: action_uri:               The uri that should be visited to
                                          complete the token action.
        :param: subject:                  The email subject.
        :param: override_access_lifespan: Overrides the JWT_ACCESS_LIFESPAN
                                          to set an access lifespan for the
                                          registration token.
        """
        notification = {
            'result': None,
            'message': None,
            'user': str(user),
            'email': email,
            'token': custom_token,
            'subject': subject,
            'confirmation_uri': action_uri,  # backwards compatibility
            'action_uri': action_uri,
        }

        PraetorianError.require_condition(
            action_sender,
            "A sender is required to send confirmation email",
        )

        PraetorianError.require_condition(
            custom_token,
            "A custom_token is required to send notification email",
        )

        #if template is None:
        with open(template) as fh:
            template = fh.read()

        with PraetorianError.handle_errors('fail sending email'):
            flask.current_app.logger.debug(
                "NOTIFICATION: {}".format(notification))

            jinja_tmpl = jinja2.Template(template)
            notification['message'] = jinja_tmpl.render(notification).strip()

            msg = Message(html=notification['message'],
                          sender=action_sender,
                          subject=notification['subject'],
                          recipients=[notification['email']])

            flask.current_app.logger.debug("Sending email to {}".format(email))
            notification['result'] = flask.current_app.extensions['mail'].send(
                msg)

        return notification