Authentication and authorization¤
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 auth
module requires an authorization server to function.
It works best with TeskaLabs Seacat Auth
authorization server
(See its documentation for setup instructions).
Getting started¤
To get started, add asab.web
module to your application and initialize asab.web.auth.AuthService
:
import asab
import asab.web
import asab.web.auth
...
class MyApplication(asab.Application):
def __init__(self):
super().__init__()
# Initialize web module
asab.web.create_web_server(self)
# Initialize authorization service
self.AuthService = asab.web.auth.AuthService(self)
Note
If your app has more than one web container, you will need to call AuthService.install(web_container)
to apply
the authorization.
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 "mock mode". See the Configuration section for details.
Every handler in your web server now accepts only requests with a valid authentication.
Unauthenticated requests are automatically answered with
HTTP 401: Unauthorized.
For every authenticated request, an asab.web.auth.Authorization
object is created and stored
in asab.contextvars.Authz
for easy access.
It contains authorization and authentication details, such as CredentialsId
, Username
or Email
, and
access-checking methods has_resource_access
, require_superuser_access
and more (see reference).
import asab.contextvars
import asab.web.rest
...
async def order_breakfast(request):
authz = asab.contextvars.Authz.get()
username = authz.Username
# This will raise asab.exceptions.AccessDeniedError when the user is not authorized for resource `breakfast:access`
authz.require_resource_access("breakfast:access")
print("{} is ordering breakfast.".format(username))
if authz.has_resource_access("breakfast:pancakes"):
print("{} can get pancakes for breakfast!".format(username))
if authz.has_superuser_access():
print("{} can get anything they want!".format(username))
return asab.web.rest.json_response(request, {
"result": "Good morning {}, your breakfast will be ready in a minute!".format(username)
})
See examples/web-auth.py for a full demo ASAB application with auth module.
Multitenancy¤
If your web container runs in multi-tenant mode, AuthService will ensure the authorization of tenant context. Read more in the Multitenancy chapter.
Configuration¤
The asab.web.auth
module is configured in the [auth]
section with the following options:
Option | Type | Meaning |
---|---|---|
enabled |
production (default), mock or development |
Switches authentication and authorization mode. In mock mode all incoming requests are authorized with mock user info without any communication with the auth server. In development mode the app is expected to run without Nginx reverse proxy and does auth introspection by itself (it is necessary to configure introspection_url ). |
public_keys_url |
URL | The URL of the authorization server's public keys (also known as jwks_uri in OAuth 2.0 |
introspection_url |
URL | The URL of Seacat Auth introspection endpoint used in development mode. |
mock_claims_path |
path | Path to JSON file that contains auth claims (aka User Info) used in mock mode. The structure of the JSON object should follow the OpenID Connect userinfo definition and also contain the resources object. |
Default values:
Without any configuration, all incoming requests will be denied with HTTP 401, since the auth service is in production mode and no public keys are provided.
Production mode¤
In production mode, the AuthService
uses the authorization server to authenticate and authorize incoming requests.
The application is expected to run behind Nginx reverse proxy with auth introspection enabled.
You need to configure public_keys_url
to point to the auth server's public keys endpoint to be able to verify incoming requests.
Internal auth¤
To use ASAB's cluster-internal authentication (together with the web authentication above),
initialize asab.api.ApiService
and asab.zookeeper.Module
in your application.
This will automatically set up shared keys necessary for internal auth, no extra configuration necessary.
You can also use internal authentication without web authentication by leaving public_keys_url
empty.
Development mode¤
In development mode, the AuthService
expects the application to run without Nginx reverse proxy and
does auth token introspection by itself.
To use the development mode, set the enabled
option to development
and specify
the introspection_url
to point to the auth server's access token introspection endpoint.
You also need to configure public_keys_url
to the auth server's public keys endpoint.
[auth]
enabled=development
public_keys_url=http://localhost:8900/.well-known/jwks.json
introspection_url=http://localhost:8900/nginx/introspect/openidconnect
Mock mode¤
In mock mode, the AuthService
authorizes all incoming requests with mock authorization claims (aka User Info) without
any communication with the auth server.
You can also customize the claims by providing a path to a JSON file with mock claims.
The file content should comply with the OpenID Connect ID token claims
and also contain the resources
object.
The iss
and exp
timestamp claims can contain relative value as a string in the format now + 30m
, now - 1d
etc.
They will be converted to absolute timestamps at runtime.
Omitted claims will be supplied with default values.
Custom claims examples¤
Custom issue and expiration time
Custom subject ID (sub) and access control (resources)
{
"sub": "abc123def456-xyz789",
"resources": {
"*": [],
"cool-eshop": ["article:edit", "article:delete"],
}
}
Reference¤
asab.web.auth.AuthService
¤
Bases: Service
Provides authentication and authorization of incoming requests.
Source code in asab/web/auth/service.py
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 |
|
authorize_request(request)
async
¤
Authenticate and authorize request with first valid provider
Parameters:
Name | Type | Description | Default |
---|---|---|---|
request
|
Request
|
Request to authenticate and authorize |
required |
Returns:
Type | Description |
---|---|
Authorization
|
Authorization object |
Raises:
Type | Description |
---|---|
NotAuthenticatedError
|
When no provider is able to authorize the request |
Source code in asab/web/auth/service.py
get_authorized_tenant(request=None)
¤
DEPRECATED. Get the request's authorized tenant.
Source code in asab/web/auth/service.py
install(web_container)
¤
Apply authorization to all web handlers in a web container.
:param web_container: Web container to be protected by authorization. :type web_container: asab.web.WebContainer
Source code in asab/web/auth/service.py
is_enabled()
¤
OBSOLETE. Check if the AuthService is enabled. Mock mode counts as enabled too.
Source code in asab/web/auth/service.py
set_up_auth_web_wrapper(aiohttp_app)
async
¤
Inspect all registered handlers and wrap them in decorators according to their parameters.
Source code in asab/web/auth/service.py
asab.web.auth.Authorization
¤
Contains authentication and authorization claims (aka UserInfo), provides methods for checking and enforcing access control.
Attributes:
Name | Type | Description |
---|---|---|
CredentialsId |
str
|
Unique identifier of the authorized entity in the ASAB ecosystem. Usually corresponds to JWT attribute "sub". |
Username |
str | None
|
End-user's preferred username. |
Email |
str | None
|
End-user email address. |
Phone |
str | None
|
End-user phone number. |
SessionId |
str
|
Sign-on session identifier. |
Issuer |
str
|
Unique identifier of the server that issued the authorization. |
IssuedAt |
datetime
|
Timestamp when the authorization was issued. |
Expiration |
datetime
|
Timestamp when the authorization expires. |
Source code in asab/web/auth/authorization.py
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 |
|
__init__(claims)
¤
Initialize Authorization object from authorization server claims.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
claims
|
dict
|
Authorization server claims (from ID token, UserInfo etc.). |
required |
Source code in asab/web/auth/authorization.py
get_claim(key, default=None)
¤
Get the value of a token claim.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
key
|
str
|
Claim name. |
required |
Returns:
Type | Description |
---|---|
Any
|
Value of the requested claim (or |
Raises:
Type | Description |
---|---|
NotAuthenticatedError
|
When the authorization is expired or otherwise invalid. |
Source code in asab/web/auth/authorization.py
has_resource_access(*resources)
¤
Check whether the agent is authorized to access requested resources.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
*resources
|
str
|
A variable number of resource IDs whose authorization is requested. |
()
|
Returns:
Name | Type | Description |
---|---|---|
bool |
bool
|
Is the agent authorized to access requested resources? |
Raises:
Type | Description |
---|---|
NotAuthenticatedError
|
When the authorization is expired or otherwise invalid. |
Examples:
>>> import asab.contextvars
>>> authz = asab.contextvars.Authz.get()
>>> if authz.has_resource_access("article:read", "article:write"):
>>> print("I can read and write articles!")
>>> else:
>>> print("Not much to do here.")
Source code in asab/web/auth/authorization.py
has_superuser_access()
¤
Check whether the agent is a superuser.
Returns:
Name | Type | Description |
---|---|---|
bool |
bool
|
Does the agent have superuser access? |
Raises:
Type | Description |
---|---|
NotAuthenticatedError
|
When the authorization is expired or otherwise invalid. |
Examples:
>>> import asab.contextvars
>>> authz = asab.contextvars.Authz.get()
>>> if authz.has_superuser_access():
>>> print("I am a superuser and can do anything!")
>>> else:
>>> print("I am but a mere mortal.")
Source code in asab/web/auth/authorization.py
has_tenant_access()
¤
Check whether the agent has access to the tenant in context.
Returns:
Name | Type | Description |
---|---|---|
bool |
bool
|
Is the agent authorized to access requested tenant? |
Raises:
Type | Description |
---|---|
NotAuthenticatedError
|
When the authorization is expired or otherwise invalid. |
Examples:
>>> import asab.contextvars
>>> authz = asab.contextvars.Authz.get()
>>> tenant_ctx = asab.contextvars.Tenant.set("big-corporation")
>>> try:
>>> if authz.has_tenant_access():
>>> print("I have access to Big Corporation!")
>>> else:
>>> print("Not much to do here.")
>>> finally:
>>> asab.contextvars.Tenant.reset(tenant_ctx)
Source code in asab/web/auth/authorization.py
is_valid()
¤
require_resource_access(*resources)
¤
Ensure that the agent is authorized to access the specified resources.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
*resources
|
str
|
A variable number of resource IDs whose authorization is requested. |
()
|
Raises:
Type | Description |
---|---|
NotAuthenticatedError
|
When the authorization is expired or otherwise invalid. |
AccessDeniedError
|
When the agent does not have access to the requested resources. |
Examples:
>>> import asab.contextvars
>>> authz = asab.contextvars.Authz.get()
>>> authz.require_resource_access("article:read", "article:write")
>>> print("I can read and write articles!")
Source code in asab/web/auth/authorization.py
require_superuser_access()
¤
Ensure that the agent has superuser access.
Raises:
Type | Description |
---|---|
NotAuthenticatedError
|
When the authorization is expired or otherwise invalid. |
AccessDeniedError
|
When the agent does not have superuser access. |
Examples:
>>> import asab.contextvars
>>> authz = asab.contextvars.Authz.get()
>>> authz.require_superuser_access()
>>> print("I am a superuser and can do anything!")
Source code in asab/web/auth/authorization.py
require_tenant_access()
¤
Ensures that the agent is authorized to access the tenant in the current context.
Raises:
Type | Description |
---|---|
NotAuthenticatedError
|
When the authorization is expired or otherwise invalid. |
AccessDeniedError
|
When the agent does not have access to the requested tenant. |
Examples:
>>> import asab.contextvars
>>> authz = asab.contextvars.Authz.get()
>>> tenant_ctx = asab.contextvars.Tenant.set("big-corporation")
>>> try:
>>> authz.require_tenant_access()
>>> print("I have access to Big Corporation!")
>>> finally:
>>> asab.contextvars.Tenant.reset(tenant_ctx)
Source code in asab/web/auth/authorization.py
require_valid()
¤
Ensure that the authorization is not expired.
Raises:
Type | Description |
---|---|
NotAuthenticatedError
|
When the authorization is expired or otherwise invalid. |
Source code in asab/web/auth/authorization.py
user_info()
¤
Return OpenID Connect UserInfo claims (or JWToken claims).
NOTE: If possible, use Authz attributes (CredentialsId, Username etc.) instead of inspecting the claims directly.
Returns:
Name | Type | Description |
---|---|---|
dict |
Dict[str, Any]
|
UserInfo claims |
Raises:
Type | Description |
---|---|
NotAuthenticatedError
|
When the authorization is expired or otherwise invalid. |
Source code in asab/web/auth/authorization.py
asab.web.auth.require(*resources)
¤
Require that the request have authorized access to one or more resources. Requests without these resources result in AccessDeniedError and consequently in an HTTP 403 response.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
resources
|
Iterable
|
Resources whose authorization is required. |
()
|
Examples:
@asab.web.auth.require("insider-info:access")
async def get_insider_info(self, request):
data = await self.service.get_insider_info()
return asab.web.rest.json_response(request, data)
Source code in asab/web/auth/decorator.py
asab.web.auth.require_superuser(handler)
¤
Require that the request have authorized access to the superuser resource. Requests without superuser access result in AccessDeniedError and consequently in an HTTP 403 response.
Examples:
@asab.web.auth.require_superuser
async def get_confidential_info(self, request):
data = await self.service.get_confidential_info()
return asab.web.rest.json_response(request, data)
Source code in asab/web/auth/decorator.py
asab.web.auth.noauth(handler)
¤
Exempt the decorated handler from authentication and authorization.
Examples:
@asab.web.auth.noauth
async def get_public_info(self, request):
data = await self.service.get_public_info()
return asab.web.rest.json_response(request, data)