Authentication

This module provides a simple setup and helper methods to authenticate users either based on HTTP Authorization header, query parameter or a cookie based session.

This addon can be included like this:

from flask import Flask
from flask_tern import auth

app = Flask(__name__)

# here is a good place to set app.config values if needed.

auth.init_app(app)

flask_tern.auth.init_app() configures authlib's flask integration. The authlib extension instance is available anytime as global object importable as:

from flask_tern.auth import oauth

# The default client is registered as "oidc"
oauth.oidc

This instance allows access to current valid tokens and other token related methods.

How it works

Main entry point to user authentication is the local proxy flask_tern.auth.current_user. This proxy defers authentication attemps to flask_tern.auth.utils.get_authorization(), which first looks for a HTTP Authorization header, which it decodes. If the header is available it will be parsed and authentication type (e.g. basic, bearer, etc…), and data will be extracted. If this header is not set, it will look for a X-Api-Key header. As a last attempt it will check whether there is a session (cookie) with valid login data.

Header formats are as usual for basic aund bearer method. The custom apikey-v1 authorization scheme ose not use base64 encoding. The accepted value for the X-Api-Key is the same as for apikey-v1 auth scheme.

Based on authentication method, the app will look into USERINFO_MAP to parse and validate authentication data. If all goes well, current_user will be a dictionary with authenticated and validated user information.

The whole authentication process also sets some request global variables in flask.g:

This whole auth process is triggered once per request by accessing flask_tern.auth.current_user.

View Decorators

The following is a list of view decorators provided by flask_tern.auth. These decorators can be used to enforce authentication for view methods. All view decorators access flask_tern.auth.current_user which kicks off the authentication process.

flask_tern.auth.require_user(view_func: Callable) Callable[[], Response | str | bytes | List[Any] | Mapping[str, Any] | Iterator[str] | Iterator[bytes] | Tuple[Response | str | bytes | List[Any] | Mapping[str, Any] | Iterator[str] | Iterator[bytes], Headers | Mapping[str, str | List[str] | Tuple[str, ...]] | Sequence[Tuple[str, str | List[str] | Tuple[str, ...]]]] | Tuple[Response | str | bytes | List[Any] | Mapping[str, Any] | Iterator[str] | Iterator[bytes], int] | Tuple[Response | str | bytes | List[Any] | Mapping[str, Any] | Iterator[str] | Iterator[bytes], int, Headers | Mapping[str, str | List[str] | Tuple[str, ...]] | Sequence[Tuple[str, str | List[str] | Tuple[str, ...]]]] | WSGIApplication]

View decorator which enforces an authenticated user.

If flask_tern.auth.current_user is not set, then call flask.abort(), which raises a 403 HTTP Exception. If we have a valid user, then execute the wrapped view method:

@require_user
def view():
    return "ok"

# noqa: DAR201, DAR101

Parameters:

view_func (Callable)

Return type:

Callable[[], Response | str | bytes | List[Any] | Mapping[str, Any] | Iterator[str] | Iterator[bytes] | Tuple[Response | str | bytes | List[Any] | Mapping[str, Any] | Iterator[str] | Iterator[bytes], Headers | Mapping[str, str | List[str] | Tuple[str, …]] | Sequence[Tuple[str, str | List[str] | Tuple[str, …]]]] | Tuple[Response | str | bytes | List[Any] | Mapping[str, Any] | Iterator[str] | Iterator[bytes], int] | Tuple[Response | str | bytes | List[Any] | Mapping[str, Any] | Iterator[str] | Iterator[bytes], int, Headers | Mapping[str, str | List[str] | Tuple[str, …]] | Sequence[Tuple[str, str | List[str] | Tuple[str, …]]]] | WSGIApplication]

flask_tern.auth.require_login(view_func: Callable) Callable[[], Response | str | bytes | List[Any] | Mapping[str, Any] | Iterator[str] | Iterator[bytes] | Tuple[Response | str | bytes | List[Any] | Mapping[str, Any] | Iterator[str] | Iterator[bytes], Headers | Mapping[str, str | List[str] | Tuple[str, ...]] | Sequence[Tuple[str, str | List[str] | Tuple[str, ...]]]] | Tuple[Response | str | bytes | List[Any] | Mapping[str, Any] | Iterator[str] | Iterator[bytes], int] | Tuple[Response | str | bytes | List[Any] | Mapping[str, Any] | Iterator[str] | Iterator[bytes], int, Headers | Mapping[str, str | List[str] | Tuple[str, ...]] | Sequence[Tuple[str, str | List[str] | Tuple[str, ...]]]] | WSGIApplication]

Redirect to login page if not logged in.

If flask_tern.auth.current_user is not set, then it will generate a redirect to a named endpoint oidc_login.login (as defined in blueprint flask_tern.auth.login.oidc_login). The current url is passed as url parameter return_url to the endpoint. After successful login, the user can be redirected back to the original url. If the user is not viewing from the web browser, we can return a 403 response.

This wrapper can only be used with simple GET requests, as passing a post body on redirect may not work:

@require_login
def view():
    return "ok"

# noqa: DAR101, DAR201

Parameters:

view_func (Callable)

Return type:

Callable[[], Response | str | bytes | List[Any] | Mapping[str, Any] | Iterator[str] | Iterator[bytes] | Tuple[Response | str | bytes | List[Any] | Mapping[str, Any] | Iterator[str] | Iterator[bytes], Headers | Mapping[str, str | List[str] | Tuple[str, …]] | Sequence[Tuple[str, str | List[str] | Tuple[str, …]]]] | Tuple[Response | str | bytes | List[Any] | Mapping[str, Any] | Iterator[str] | Iterator[bytes], int] | Tuple[Response | str | bytes | List[Any] | Mapping[str, Any] | Iterator[str] | Iterator[bytes], int, Headers | Mapping[str, str | List[str] | Tuple[str, …]] | Sequence[Tuple[str, str | List[str] | Tuple[str, …]]]] | WSGIApplication]

flask_tern.auth.require_roles(roles: Iterable[str]) Callable[[Callable], Callable]

Require an authenticated user with a specific set of roles.

This wrapper ensures that there is an authenticated user, and all roles set in this decorator are present on the current users role attribute:

@require_roles(["admin", "writer"])
def view():
    return "ok"

# noqa: DAR101, DAR201

Parameters:

roles (Iterable[str])

Return type:

Callable[[Callable], Callable]

Auth Settings

Authentication setup requires a few configuration settings.

General

flask_tern.auth.settings.USERINFO_MAP

A set of methods to extract user information based on authentication method. E.g. bearer looks at HTTP Authorization header with bearer tokens, and session uses sessions and cookies to store curretn login status. Other options can easily be implemented. See Authentication for details

This dictionary is used to extract user details from request depending on authentication method.

USERINFO_MAP default values

Auth method

default extractor

bearer

flask_tern.auth.oidc.get_userinfo_from_accesstoken()

session

flask_tern.auth.login.get_userinfo_from_session()

apikey-v1

flask_tern.auth.apikey.get_userinfo_from_apikey()

OpenID Connect

flask_tern.auth.settings.OIDC_DISCOVERY_URL = None

The URL which serves an OIDC discovery document. This usually ends with /.well-known/openid-configuration.

flask_tern.auth.settings.OIDC_CLIENT_ID = None

OIDC client id

flask_tern.auth.settings.OIDC_CLIENT_SECRET = None

OIDC client secret

flask_tern.auth.settings.OIDC_SCOPE = 'openid profile email'

OIDC scope. This needs to include openid to enable OpenID Connect protocol.

flask_tern.auth.settings.OIDC_USE_REFRESH_TOKEN = False

If set to true, the application will store a refresh token in the session, and will keep the access and id token valid (until refresh token or session timeout)

Api Key Authentication

flask_tern.auth.settings.APIKEY_SERVICE = None

5000/api/v1.0. The URL must not end with /.

Type:

The API endpoint of the API Key service e.g. http

Type:

//localhost

Authentication methods

This section describes the supported authentication methods in detail.

OpenID Connect Session

The module flask_tern.auth.login provides a flask Blueprint to support session based login via browsers as well.

from flask_tern.auth.login import oidc_login

app = Flask(__name__)

app.register_blueprint(oidc_login, url_prefix="/api/oidc")

This setup will configure 3 url endpoints.

  • /api/oidc/login: start OIDC login dance

  • /api/oidc/authorize: return url from identity provider

  • /api/oidc/logout: clears current users session triggering a logout

See the respective view functions:

flask_tern.auth.login.login()

Generate redirect url for oauth login.

accepts url parameter return_url or uses Referrer header to remember return_url

If there is no return_url set, it use the url for a route named root, which needs to be configured on the Flask app.

Returns:

Redirect to authorize url.

flask_tern.auth.login.authorize()

Parse and validate authorization response.

On success return redirect user back to original URL and set session cookie

If there is no return_url set, it use the url for a route named root, which needs to be configured on the Flask app.

Returns:

Redirect to stored return url, or application root route.

flask_tern.auth.login.logout()

Clear logged in session.

If there is no return_url set, it use the url for a route named root, which needs to be configured on the Flask app.

Returns:

Redirect to return url.

After successful login, authenticated user info will be stored in the session under key oidc.user_info. Best way to retrieve this data is by accessing flask_tern.auth.current_user.

If the setting OIDC_USE_REFRESH_TOKEN is enable (set to True), the application will keep OIDC id-, acces-, and refresh token up to date in the session. This means that the application code can use these tokens anytime to call out to additional services accepting these tokens. If token refresh is enabled, all current tokens are stored under oidc.tokens in the session, from where they are picked up by the authlib Flask integration.

If the view flask_tern.auth.login.authorize() fails to authorise the OIDC token it will return a redirect to either previous url, or application root. Additionally a cookie auth_error will be set in the response carrying the error message.

Session handling

The application integrates Flask-Session which offers a configurable session backend for flask applications. Please see Flask-Session Configuration section for details.

Common configuration values:

flask_tern.settings.SECRET_KEY = os.urandom(16)

value to use to sign / encrypt session cookie. If unset, the app will auto generate a value. This value needs to be configured if load balancing across multiple processes is used.

flask_tern.settings.SESSION_TYPE = None

Session backend type. The default is to use cookies as session storage.

flask_tern.settings.SESSION_FILE_DIR = '/Users/uqgweis/Documents/Projects/workspaces/bioimages/flask_tern/docs/flask_session'

If SESSION_TYPE = “filesystem”, this option specifies the storage location for session data. as default it uses the flask_session folder within current working directory.

flask_tern.settings.SESSION_FILE_THRESHOLD = 500

If SESSION_TYPE = “filesystem”, this option sets the maximum number of items the session stores before it starts deleting some. default is 500.

flask_tern.settings.SESSION_FILE_MODE = 384

If ‘SESSION_TYPE = “filesystem”` this option sets the file mode for the session files. as default it uses 0o600. If given as string it will be parsed as integer or if prefixed with 0o as octal number.

flask_tern.settings.SESSION_REDIS = None

If SESSION_TYPE = “redis”, this option should be a configured redis.Redis instance. If loaded from environment variables, it can be specified as connection url as described in redis:redis.from_url()

OAuth2 Bearer token

A request may contain an OAuth2 bearer token in the Authorization header. It assumed that the token provided is a JWT encoded access_token (as provided by keycloak) and user info can be extracted directly from the token.

See flask_tern.auth.oidc.get_oauth_userinfo() or details.

API Key

This method relies on a deployed API Key service. This method asks the API Key service to validate the given token. The API Key service is expected to return user info and roles, in the same format as Keycloak produces access tokens.

This method utilises flask_tern.cache to cache validation results. This is done to improve response time of subsequent request with API Keys. To enable caching it is necessary to configure Flask-Caching properly.

# e.g.: use simple caching
CACHE_TYPE = "SimpleCache"

See flask_tern.auth.apikey.get_userinfo_from_apikey() for details.