Authorization and multitenancy¶
The asab.web.auth
module provides authentication
and authorization of incoming requests.
This enables your application to differentiate between users, grant or deny them API access depending
on their permissions and work with other relevant user data.
The module also implements multitenancy, meaning that your app can be used by a number of independent subjects (tenants, for example companies) without interfering with each other.
The auth
module requires an authorization server to function. It works best
with Seacat Auth authorization server
(See its documentation for setup instructions).
Getting started¶
To get started, initialize AuthService
and install it in your asab.web.WebContainer
:
class MyApplication(asab.Application):
def __init__(self):
super().__init__()
# Initialize web container
self.add_module(asab.web.Module)
self.WebService = self.get_service("asab.WebService")
self.WebContainer = asab.web.WebContainer(self.WebService, "web")
# Initialize authorization service and install the decorators
self.AuthService = asab.web.auth.AuthService(self)
self.AuthService.install(self.WebContainer)
Note
You may also need to specify your authorization server’s
public_keys_url
(also known as jwks_uri
in OAuth 2.0).
In case you don’t have any authorization server at hand, you can run the auth module in dev_mode
.
See the Configuration section for details.
Every handler in WebContainer
now accepts only requests with a valid authentication.
Unauthenticated requests are automatically answered with
HTTP 401 Unauthorized.
Authenticated requests will be inspected for user info, authorized user tenant and resources.
These attributes are passed to the handler method, if the method in question has the corresponding keyword arguments
(user_info
, tenant
and resources
).
In the following example, the method will receive user_info
and resources
from the request:
async def order_breakfast(self, request, *, tenant, user_info, resources):
user_id = user_info["sub"]
user_name = user_info["preferred_username"]
if "pancakes:eat" in resources:
...
return asab.web.rest.json_response(request, {"result": "Your breakfast is being prepared!"})
See examples/web-auth.py for a full demo ASAB application with auth module.
Configuration¶
The asab.web.auth
module is configured in the [auth]
section with the following options:
-
public_keys_url
¶
(URL, default: http://localhost:8081/openidconnect/public_keys)
The URL of the authorization server’s public keys (also known asjwks_uri
in OAuth 2.0).
-
multitenancy
¶
(boolean, default: yes)
Toggles the behavior of endpoints with configurable tenant parameter. When enabled, the tenant query paramerter is required. When disabled, the tenant query paramerter is ignored and set toNone
. In dev mode, the multitenancy switch is ignored and the tenant parameter is taken into account only when it is present in query, otherwise it is set toNone
.
-
dev_mode
¶
(boolean, default: no)
In dev mode, all incoming requests are authenticated and authorized with mock user info. There is no communication with authorization server (so it is not necessary to configurepublic_keys_url
in dev mode).
-
dev_user_info_path
¶
(path, default: /conf/dev-userinfo.json)
Path to JSON file that contains mock user info used in dev mode. The structure of user info should follow the OpenID Connect userinfo definition and also contain theresources
object.
Multitenancy¶
Strictly multitenant endpoints¶
Strictly multitenant endpoints always operate within a tenant, hence they need the tenant parameter to be always provided. Such endpoints must define the tenant parameter in their URL path and include tenant argument in the handler method. Auth service extracts the tenant from the URL path, validates the tenant existence, checks if the request is authorized for the tenant and finally passes the tenant name to the handler method.
Example handler:
class MenuHandler:
def __init__(self, app):
self.MenuService = app.get_service("MenuService")
router = app.WebContainer.WebApp.router
# Add a path with `tenant` parameter
router.add_get("/{tenant}/todays-menu", self.get_todays_menu)
# Define handler method with `tenant` argument in the signature
async def get_todays_menu(self, request, *, tenant):
menu = await self.MenuService.get_todays_menu(tenant)
return asab.web.rest.json_response(request, data=menu)
Example request:
GET http://localhost:8080/lazy-raccoon-bistro/todays-menu
Configurably multitenant endpoints¶
Configurably multitenant endpoints usually operate within a tenant, but they can also operate in tenantless mode if
the application is configured so. When you create an endpoint without tenant
parameter in the URL path and
with tenant
argument in the handler method, the Auth service will either expect the tenant
parameter to be
provided in the URL query if mutlitenancy is enabled, or to not be provided at all if multitenancy is disabled.
Use the multitenancy
boolean switch in the [auth]
config section to control the multitenancy setting.
- If multitenancy is enabled, the request’s query string must include a tenant parameter. Requests without
- the tenant query result in Bad request (HTTP 400).
- If multitenancy is disabled, the tenant argument in the handler method is set to
None
. - Any
tenant
parameter in the query string is ignored.
- If multitenancy is disabled, the tenant argument in the handler method is set to
Example handler:
class MenuHandler:
def __init__(self, app):
self.MenuService = app.get_service("MenuService")
router = app.WebContainer.WebApp.router
# Add a path without `tenant` parameter
router.add_get("/todays-menu", self.get_todays_menu)
# Define handler method with `tenant` argument in the signature
async def get_todays_menu(self, request, *, tenant):
menu = await self.MenuService.get_todays_menu(tenant)
return asab.web.rest.json_response(request, data=menu)
Example request (with multitenancy on):
GET http://localhost:8080/todays-menu?tenant=lazy-raccoon-bistro
Example request (with multitenancy off):
GET http://localhost:8080/todays-menu
Development mode¶
In dev mode, actual authorization is disabled and replaced with mock authorization. Useful for developing ASAB apps
without authorization server. Activate by enabling dev_mode
in the [auth]
config section. You can also specify
a path to a JSON file with your own mocked userinfo payload [auth] dev_user_info_path
:
[auth]
dev_mode=yes
dev_user_info_path=${THIS_DIR}/mock_userinfo.json
When dev mode is enabled, you don’t have to provide public_keys_url as this option is ignored.