FlaskSimpleAuth Recipes

Here are a few task-oriented recipes with FlaskSimpleAuth. Feel free to submit your questions!

Installation

How-to install FlaskSimpleAuth?

FlaskSimpleAuth is a Python module available from PyPI. Install it, for instance with pip, with the required dependencies:

pip install FlaskSimpleAuth[password,jwt,cors,redis]

Authentication

For details, see the relevant authentication section in the documentation.

How-to configure basic authentication?

HTTP Basic authentication is a login and password authentication encoded in base64 into an Authorization HTTP header.

  • set FSA_AUTH to basic or to contain basic, or have a route with a auth="basic" parameter.

  • register a get_user_pass hook.

  • simple authentication routes are triggered with authorize="AUTH"

How-to configure parameter authentication?

This is login and password authentication passed as HTTP or JSON parameters.

  • set FSA_AUTH to param or to contain param, or have a route with a auth="param" parameter.

  • the name of the expected two parameters are set with FSA_PARAM_USER and FSA_PARAM_PASS.

  • register a get_user_pass hook.

  • simple authentication routes are triggered with authorize="AUTH"

How-to configure token authentication?

It is enabled by default if FSA_MODE is a scalar. It can be enabled explicitely with FSA_MODE as a list which contains token.

If you do not really need JWT compatibility, keep the default fsa token type (FSA_TOKEN_TYPE) which is human readable, unlike JWT.

How to disable token authentication?

  • set FSA_AUTH to the list of authentication schemes, which must not contain token.

  • set FSA_TOKEN_TYPE to None.

How-to get the current user login as a string?

There are several equivalent options:

  • use a special CurrentUser parameter type on a route to retrieve the user name.

  • call app.current_user() on an authenticated route.

  • call app.get_user() on any route, an authentification will be attempted.

How-to get the current user as an object?

You must build the object yourself, based on the string user name.

  • with a function of your making:

    def get_user_object():
        return UserObject(app.current_user())
    
  • for convenience, this function can be registered as a special parameter associated to the type:

    app.special_parameter(UserObject, lambda _: get_user_object())
    
    @app.route("/...", authorize="AUTH")
    def route_dotdotdot(user: UserObject)
        ...
    

How-to store login and passwords?

Passwords must be stored as cryptographic salted hash, so as to deter hackers from recovering passwords too easily of the user database is leaked.

FlaskSimpleAuth provides state-of-the-art settings by default using bcrypt with 4 rounds (about 2⁴ hash calls) and ident 2y. Keep that unless you really have a strong opinion against it.

The number of rounds is kept as low as allowed by the library because cryptographic password functions are very expensive, eg with 12 rounds, which is passlib default, results in several 100 ms computation time, which for a server is astronomically high.

Method app.hash_password(…) applies hashes the passwords according to the current configuration. The get_user_pass hook will work as expected if it returns such a value stored from a previous call.

How-to implement my own authentication scheme?

You need to create a callback to handle your scheme:

  • create a function which returns the login based on the app and request:

    def xyz_authentication(app, req):
        # investigate the request and return the login or None for 401
        # possibly raise an ErrorResponse with fsa.err(…)
        return ...
    
  • register this callback as an authentication scheme:

    app.authentication("xyz", xyz_authentication)
    
  • use this new authentication method in FSA_AUTH or maybe on some route with an auth="xyz" parameter.

How-to test authentication and authorizations without any password?

Use FSA_AUTH="fake" and pass the expected login as a request parameter (FSA_FAKE_LOGIN, defaults to LOGIN).

Fake authentication is only allowed for localhost connections and cannot be deployed on a real server.

How-to use LDAP/AD authentication?

The AD password checking model is pretty strange, as it requires to send the clear password to the authentication server to check whether it is accepted. To do that at the library level:

  • create a new password checking function:

    def check_login_password_with_AD_server(login: str, password: str) -> bool|None:
        import ldap
        # connect to server... send login/pass... look for result...
        return ...
    
    • on True: the password is accepted

    • on False: it is not

    • on None: 401 (no such user)

    • if unhappy: raise an ErrorResponse exception with fsa.err(...)

  • register this hook

    app.password_check(check_login_password_with_AD_server)
    
  • you do not need to have a get_user_pass hook if this is the sole password scheme used by your application.

Alternatively, this could be implemented at the application level with one route which checks the credentials and provides a token which will be used afterwards:

  • create the token route:

    # this route is open in the sense that it takes charge of checking credentials
    @app.post("/token-ad", authorize="OPEN", auth="none")
    def get_token_ad(username: str, password: str):
        if not check_login_password_with_AD_server(username, password):
            fsa.err("invalid AD credentials", 401)
        return {"token": app.create_token(username)}, 201
    
  • use FSA_AUTH_DEFAULT="token" so that all other routes require the token.

How-to use multi-factor authentication (MFA)?

The idea is to rely on an intermediate token with a temporary realm to validate that an authenfication method has succeeded, and that another must still be checked.

  • create a route with the first auth method, eg a login/password basic authentication.

    @app.get("/login1", authorize="AUTH", auth="basic")
    def get_login1(user: fsa.CurrentUser):
        # trigger sending an email or SMS for a code
        send_temporary_code_to_user(user)
        # 10 minutes token provided with this basic authentication
        return app.create_token(user, realm="login1", delay=10.0), 200
    
  • create a route protected by the previous token, and check the email or SMS code provided by the user at this stage.

    @app.get("/login2", authorize="AUTH", auth="token", realm="login1")
    def get_login2(user: fsa.CurrentUser, code: str):
        if not check_code_validity(user, code):
            return "invalid validation code", 401
        # else return the final token
        return app.create_token(user), 200
    
  • only allow token authentication on other routes, eg with FSA_AUTH_DEFAULT = "token".

See MFA demo.

How to ensure that no route is without authentication?

With belt and suspenders:

  • do not use authorize="OPEN" on any route.

  • use an explicit FSA_AUTH list setting without none.

  • use FSA_AUTH_DEFAULT="token".

How-to … authentication?

Authorization

For details, see the relevant authorization section in the documentation.

How-to (temporarily) close a route?

Use authorize="CLOSE" as a route parameter to trigger a 403 forbidden response.

This can be added in a list of authorizations (authorize=[…, "CLOSE"]) and will supersede all other settings for this route.

How-to open a route?

Use authorize="OPEN" as a route parameter, and allow authentication none by adding it to FSA_AUTH. Depending on FSA_AUTH_DEFAULT, you may have to add auth="none" on the route so that this (non) authentication is allowed. Anyone can execute such routes, without authentication.

How-to just authenticate a route?

Use authorize="AUTH" as a route parameter. All authenticated users can execute the route.

How-to use simple group authorizations?

Group authorizations are permissions based on belonging to a group. For that, you must:

  • create a callback to check whether a user belongs to a group:

    def user_is_admin(login: str) -> bool:
        return ... # whether user belong to the admin group
    
  • register this callback as a group_check hook:

    app.group_check("admin", user_is_admin)
    
  • declare that the group authorization is required on a route definition:

    @app.route("/admin", authorize="admin")
    def ...
    

How-to factor-out all group authorizations?

Use the generic user_in_group hook instead of per-group checks.

How-to use object authorizations?

In most application, access permissions depend on some kind of relationship to the data, e.g. someone may read a message because they are either the recipient or the sender of this particular message.

  • object authorizations require a per-domain callback which tells whether an identified object of the domain can be access by a user in a role.

    # domain "stuff" permission callback
    def stuff_permissions(stuff_id: int, login: str, role: str):
        return ...
    
  • this callback must be registered to the application

    app.object_perms("stuff", stuff_permissions)
    

    Registering can also be done with object_perms used as a decorator or through the FSA_OBJECT_PERMS directive.

  • a function must require these permission by setting authorize to a tuple, with the domain, the name of the variable which identifies the object, and the role.

    @app.patch("/stuff/<id>", authorize=("stuff", "id", "update"))
    def patch_stuff_id(id: int, ...):
        return ...
    

    Convenient defaults are provided: the first parameter for the identifier, None for the role.

How-to use oauth authorizations?

How-to … authorizations?

Parameters

How-to use pydantic or dataclasses in requests and responses?

Pydantic and dataclass classes are well integrated both for input parameters and route outputs:

  • request parameters work out of the box with through JSON:

    import pydantic
    
    # this also works with pydantic and standard dataclasses
    class User(pydantic.BaseModel):
        login: str
        firstname: str
        lastname: str
    
    @app.post("/users", authorize="ADMIN")
    def post_users(user: User):
        # user.login, user.firtname, user.lastname…
        return "", 201
    
  • responses must be processed through FlaskSimpleAuth’s jsonify:

    @app.get("/users/<uid>", authorize="ADMIN")
    def get_users_uid(uid: int):
        user: User = whatever(uid)
        return fsa.jsonify(user), 200
    

See types demo.

How-to use generic types in requests and responses?

Just use them! They are converted from/to JSON, but for list[*] with HTTP parameters are expected to be repeated parameters.

@app.get("/generic", authorize="OPEN")
def get_generic(data: dict[str, list[int]]):
    # data["foo"][0]
    return {k: len(v) for k, v in data.items()}

The generic type support is not perfect, consider using data classes instead. Simple standard types are expected, they cannot be mixed with data classes in general, although some instances may work.

How-to … parameters?

Miscellaneous

How-to allow non-TLS connections?

The default configuration emphasizes security, so non TLS connections are rejected, unless running on localhost for tests. To allow non-TLS connections, set FSA_SECURE = False and feel deeply ashamed to have done that.

How-to use a shared REDIS cache?

FlaskSimpleAuth uses redis to deal with redis.

  • if necessary, install the module: pip install redis

  • set FSA_CACHE = "redis"

  • set FSA_CACHE_OPTS so that the application can connect to the cache, eg:

    FSA_CACHE_OPTS = { "host": "my.redis.server", "port": 6379 }
    
  • if the service is shared, set FSA_CACHE_PREFIX to the application name to avoid cache entry collisions:

    FSA_CACHE_PREFIX = "acme."
    

How-to get an anwer?

Submit your question here!

How-to contribute?

Implement a feature, improve the code, fix a bug… and submit a pull request.