From a2c98316edbbe4b3a001b37bbf753afffbfed93c Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Sun, 5 Oct 2025 07:20:29 +0200 Subject: [PATCH 01/14] Start --- blacksheep/docs/authentication.md | 50 ++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/blacksheep/docs/authentication.md b/blacksheep/docs/authentication.md index 6fd50db..6665ccc 100644 --- a/blacksheep/docs/authentication.md +++ b/blacksheep/docs/authentication.md @@ -7,24 +7,11 @@ covers: - [X] How to use the built-in authentication strategy. - [X] How to configure a custom authentication handler. -- [X] How to use the built-in support for JWT Bearer authentication. +- [X] How to use the built-in support for **API Key** authentication. +- [X] How to use the built-in support for **Basic authentication**. +- [X] How to use the built-in support for **JWT Bearer** authentication. - [X] How to read the user's context in request handlers. -/// admonition | Additional dependencies. - type: warning - -Using JWT Bearer and OpenID integrations requires additional dependencies. -Install them by running: `pip install blacksheep[full]`. - -/// - -## Underlying library - -The authentication and authorization logic for BlackSheep is packaged and -published in a dedicated library: -[`guardpost`](https://github.com/neoteroi/guardpost) ([in -pypi](https://pypi.org/project/guardpost/)). - ## How to use built-in authentication Common strategies for identifying users in web applications include: @@ -38,10 +25,20 @@ Bearer tokens and then describe how to write a custom authentication handler. /// admonition | Terms: user, service, principal. -The term 'user' typically refers to human users, while 'service' describes non-human clients. In Java and .NET, the term 'principal' is commonly used to describe a generic identity. +The term 'user' typically refers to human users, while 'service' describes +non-human clients. In Java and .NET, the term 'principal' is commonly used to +describe a generic identity. /// +## API Key authentication + + + +## Basic authentication + + + ## OIDC BlackSheep implements built-in support for OpenID Connect authentication, @@ -373,3 +370,22 @@ While authentication focuses on *identifying* users, authorization determines whether a user *is permitted* to perform the requested action. The next page describes the built-in [authorization strategy](authorization.md) in BlackSheep. + + From 6496c3e1d56a0db2c3a6a1e005d22cd8f5dd1b7c Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Sun, 5 Oct 2025 07:44:42 +0200 Subject: [PATCH 02/14] Update authentication.md --- blacksheep/docs/authentication.md | 67 +++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/blacksheep/docs/authentication.md b/blacksheep/docs/authentication.md index 6665ccc..00a872d 100644 --- a/blacksheep/docs/authentication.md +++ b/blacksheep/docs/authentication.md @@ -33,10 +33,77 @@ describe a generic identity. ## API Key authentication +The following example illustrates how API Key authentication can be enabled +in BlackSheep: + +```python +from blacksheep import Application, get +from blacksheep.server.authentication.apikey import APIKey, APIKeyAuthentication +from blacksheep.server.authorization import auth +from essentials.secrets import Secret + + +app = Application() + + +app.use_authentication().add( + APIKeyAuthentication( + APIKey( + secret=Secret("$API_SECRET"), # ⟵ obtained from API_SECRET env var + roles=["user"], # ⟵ optional roles + ), + param_name="X-API-Key", + ) +) + +app.use_authorization() +@auth() # requires authorization +@get("/") +async def get_claims(request): + return request.user.roles +``` + ## Basic authentication +```python +from blacksheep import Application, get +from blacksheep.server.authentication.basic import BasicAuthentication, BasicCredentials +from blacksheep.server.authorization import auth +from essentials.secrets import Secret + + +app = Application() + + +admin_credentials = + +print(admin_credentials.to_header_value()) + +app.use_authentication().add( + BasicAuthentication( + BasicCredentials( + username="admin", + password=Secret("$ADMIN_PASSWORD"), # ⟵ obtained from ADMIN_PASSWORD env var + roles=["admin"], # ⟵ optional roles + ), + BasicCredentials( + username="user", + password=Secret("$USER_PASSWORD"), # ⟵ obtained from USER_PASSWORD env var + roles=["user"], # ⟵ optional roles + ) + ) +) + +app.use_authorization() + + +@auth() # requires authorization +@get("/") +async def get_claims(request): + return request.user.roles +``` ## OIDC From 2da3d8f572cf3dad0f64c95a9080b1b3e9eb1347 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Sun, 5 Oct 2025 10:01:02 +0200 Subject: [PATCH 03/14] Update authentication.md --- blacksheep/docs/authentication.md | 265 +++++++++++++++++++++++++++++- 1 file changed, 256 insertions(+), 9 deletions(-) diff --git a/blacksheep/docs/authentication.md b/blacksheep/docs/authentication.md index 00a872d..a97b492 100644 --- a/blacksheep/docs/authentication.md +++ b/blacksheep/docs/authentication.md @@ -33,8 +33,13 @@ describe a generic identity. ## API Key authentication -The following example illustrates how API Key authentication can be enabled -in BlackSheep: +Since version `2.4.2`, BlackSheep provides built-in support for API Key authentication +with flexible configuration options. API Keys can be read from request headers, query +parameters, or cookies, and each key can be associated with specific roles and claims. + +### Enabling API Key authentication + +The following example illustrates how API Key authentication can be enabled: ```python from blacksheep import Application, get @@ -65,21 +70,215 @@ async def get_claims(request): return request.user.roles ``` -## Basic authentication +You can configure multiple API Keys with different roles and claims: ```python from blacksheep import Application, get -from blacksheep.server.authentication.basic import BasicAuthentication, BasicCredentials +from blacksheep.server.authentication.apikey import APIKey, APIKeyAuthentication from blacksheep.server.authorization import auth from essentials.secrets import Secret app = Application() +app.use_authentication().add( + APIKeyAuthentication( + # Admin API key with full access + APIKey( + secret=Secret("$ADMIN_API_KEY"), + roles=["admin", "user"], + claims={"department": "IT"} + ), + # Regular user API key + APIKey( + secret=Secret("$USER_API_KEY"), + roles=["user"], + claims={"department": "sales"} + ), + # Read-only API key + APIKey( + secret=Secret("$READONLY_API_KEY"), + roles=["readonly"], + claims={} + ), + param_name="X-API-Key", + ) +) + +app.use_authorization() + + +@auth() +@get("/") +async def get_user_info(request): + return { + "roles": request.user.roles, + "claims": request.user.claims + } +``` + +### API Key locations + +API Keys can be retrieved from different locations in the request: + +/// tab | Header (default) + select: True + +```python +app.use_authentication().add( + APIKeyAuthentication( + APIKey(secret=Secret("your-secret-key")), + param_name="X-API-Key", + location="header" # Default location + ) +) +``` + +Test with: `curl -H "X-API-Key: your-secret-key" http://localhost:8000/` + +/// + +/// tab | Query + +```python +app.use_authentication().add( + APIKeyAuthentication( + APIKey(secret=Secret("your-secret-key")), + param_name="api_key", + location="query" + ) +) +``` + +Test with: `curl http://localhost:8000/?api_key=your-secret-key` -admin_credentials = +/// -print(admin_credentials.to_header_value()) +/// tab | Cookie + +```python +app.use_authentication().add( + APIKeyAuthentication( + APIKey(secret=Secret("your-secret-key")), + param_name="api_key", + location="cookie" + ) +) +``` + +Test with: `curl -b "api_key=your-secret-key" http://localhost:8000/` + +/// + +### Dynamic API Key provider + +For scenarios where API Keys need to be retrieved dynamically (e.g., from a database), +implement the `APIKeysProvider` abstract class: + +```python +from typing import List +from blacksheep import Application, get +from blacksheep.server.authentication.apikey import ( + APIKey, + APIKeyAuthentication, + APIKeysProvider +) +from blacksheep.server.authorization import auth +from essentials.secrets import Secret + + +class DatabaseAPIKeysProvider(APIKeysProvider): + """ + Example provider that retrieves API keys from a database. + """ + + def __init__(self, db_connection): + self.db = db_connection + + async def get_keys(self) -> List[APIKey]: + """ + Fetch API keys from database with associated roles and claims. + """ + # Example database query (adapt to your database) + keys_data = await self.db.fetch_all(""" + SELECT secret, roles, department, access_level + FROM api_keys + WHERE is_active = true + """) + + api_keys = [] + for row in keys_data: + api_keys.append(APIKey( + secret=Secret(row["secret"], direct_value=True), + roles=row["roles"].split(",") if row["roles"] else [], + claims={ + "department": row["department"], + "access_level": row["access_level"] + } + )) + + return api_keys + + +# Usage with dynamic provider +app = Application() + +# Assume you have a database connection +# db_connection = get_database_connection() + +app.use_authentication().add( + APIKeyAuthentication( + param_name="X-API-Key", + keys_provider=DatabaseAPIKeysProvider(db_connection) + ) +) + +app.use_authorization() + + +@auth() +@get("/") +async def protected_endpoint(request): + return { + "message": "Access granted", + "user_department": request.user.claims.get("department"), + "access_level": request.user.claims.get("access_level") + } +``` + +**Note:** dependency injection is also supported, configuring the +authentication handler as a _type_ to be instantiated rather than an instance. + +### Advanced API Key configuration + +You can customize the authentication scheme and add descriptions: + +```python +app.use_authentication().add( + APIKeyAuthentication( + APIKey( + secret=Secret("$API_SECRET"), + roles=["service"], + claims={"client_type": "external_service"} + ), + param_name="X-Service-Key", + scheme="ServiceKey", # Custom scheme name + location="header", + description="External service authentication using API keys" + ) +) +``` + +## Basic authentication + +```python +from blacksheep import Application, get +from blacksheep.server.authentication.basic import BasicAuthentication, BasicCredentials +from blacksheep.server.authorization import auth +from essentials.secrets import Secret + + +app = Application() app.use_authentication().add( BasicAuthentication( @@ -105,7 +304,6 @@ async def get_claims(request): return request.user.roles ``` - ## OIDC BlackSheep implements built-in support for OpenID Connect authentication, @@ -425,12 +623,61 @@ def home(request: Request): /// admonition | ContainerProtocol. type: tip -As documented in [_Container Protocol_](./dependency-injection.md#the-container-protocol), BlackSheep -supports the use of other DI containers as replacements for the built-in +As documented in [_Container Protocol_](./dependency-injection.md#the-container-protocol), +BlackSheep supports the use of other DI containers as replacements for the built-in library used for dependency injection. /// + +### Error handling and security considerations + +When using authentication and authorization, consider these security practices: + +```python +from blacksheep import Application, get, json +from blacksheep.server.authentication... +from blacksheep.exceptions import Unauthorized +from essentials.secrets import Secret + + +app = Application() + +app.use_authentication().add( + ... +) + +app.use_authorization() + + +@get("/public") +async def public_endpoint(): + """Public endpoint that doesn't require authentication.""" + return {"message": "This is public"} + + +@auth() +@get("/protected") +async def protected_endpoint(request): + """Protected endpoint that requires an authenticated user.""" + return { + "message": "Access granted", + "roles": request.user.roles + } + + +@get("/optional-auth") +async def optional_auth_endpoint(request): + """Endpoint with optional authentication.""" + if request.user and request.user.is_authenticated(): + return { + "message": "Authenticated user", + "roles": request.user.roles + } + else: + return {"message": "Anonymous user"} +``` + ## Next While authentication focuses on *identifying* users, authorization determines From b8cd8ebb80a661fe4447e6c9f4e28cc021c00267 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Sun, 5 Oct 2025 10:22:54 +0200 Subject: [PATCH 04/14] Update authentication.md --- blacksheep/docs/authentication.md | 280 ++++++++++++++++++++++++++---- 1 file changed, 243 insertions(+), 37 deletions(-) diff --git a/blacksheep/docs/authentication.md b/blacksheep/docs/authentication.md index a97b492..6e12c62 100644 --- a/blacksheep/docs/authentication.md +++ b/blacksheep/docs/authentication.md @@ -121,54 +121,47 @@ async def get_user_info(request): API Keys can be retrieved from different locations in the request: -/// tab | Header (default) - select: True +=== "Header (default)" -```python -app.use_authentication().add( - APIKeyAuthentication( - APIKey(secret=Secret("your-secret-key")), - param_name="X-API-Key", - location="header" # Default location + ```python + app.use_authentication().add( + APIKeyAuthentication( + APIKey(secret=Secret("your-secret-key")), + param_name="X-API-Key", + location="header" # Default location + ) ) -) -``` - -Test with: `curl -H "X-API-Key: your-secret-key" http://localhost:8000/` + ``` -/// + Test with: `curl -H "X-API-Key: your-secret-key" http://localhost:8000/` -/// tab | Query +=== "Query" -```python -app.use_authentication().add( - APIKeyAuthentication( - APIKey(secret=Secret("your-secret-key")), - param_name="api_key", - location="query" + ```python + app.use_authentication().add( + APIKeyAuthentication( + APIKey(secret=Secret("your-secret-key")), + param_name="api_key", + location="query" + ) ) -) -``` - -Test with: `curl http://localhost:8000/?api_key=your-secret-key` + ``` -/// + Test with: `curl http://localhost:8000/?api_key=your-secret-key` -/// tab | Cookie +=== "Cookie" -```python -app.use_authentication().add( - APIKeyAuthentication( - APIKey(secret=Secret("your-secret-key")), - param_name="api_key", - location="cookie" + ```python + app.use_authentication().add( + APIKeyAuthentication( + APIKey(secret=Secret("your-secret-key")), + param_name="api_key", + location="cookie" + ) ) -) -``` - -Test with: `curl -b "api_key=your-secret-key" http://localhost:8000/` + ``` -/// + Test with: `curl -b "api_key=your-secret-key" http://localhost:8000/` ### Dynamic API Key provider @@ -271,6 +264,16 @@ app.use_authentication().add( ## Basic authentication +Since version `2.4.2`, BlackSheep provides built-in support for HTTP Basic +Authentication, which allows clients to authenticate using a username and password +combination. Basic authentication credentials can be configured statically or retrieved +dynamically from external sources. + +### Enabling Basic authentication + +The following example shows how to configure Basic authentication with static +credentials: + ```python from blacksheep import Application, get from blacksheep.server.authentication.basic import BasicAuthentication, BasicCredentials @@ -304,6 +307,209 @@ async def get_claims(request): return request.user.roles ``` +You can configure multiple users with different roles and claims: + +```python +from blacksheep import Application, get +from blacksheep.server.authentication.basic import BasicAuthentication, BasicCredentials +from blacksheep.server.authorization import auth +from essentials.secrets import Secret + + +app = Application() + +app.use_authentication().add( + BasicAuthentication( + # Admin user with full access + BasicCredentials( + username="admin", + password=Secret("$ADMIN_PASSWORD"), + roles=["admin", "user"], + claims={"department": "IT", "level": "admin"} + ), + # Regular user + BasicCredentials( + username="john_doe", + password=Secret("$JOHN_PASSWORD"), + roles=["user"], + claims={"department": "sales", "level": "user"} + ), + # Read-only user + BasicCredentials( + username="guest", + password=Secret("$GUEST_PASSWORD"), + roles=["readonly"], + claims={"department": "public", "level": "guest"} + ) + ) +) + +app.use_authorization() + + +@auth() +@get("/") +async def get_user_info(request): + return { + "username": request.user.claims.get("sub"), + "roles": request.user.roles, + "claims": request.user.claims + } +``` + +Test with curl: +```bash +# Admin user +curl -u "admin:admin_password_here" http://localhost:8000/ + +# Regular user +curl -u "john_doe:john_password_here" http://localhost:8000/ + +# Guest user +curl -u "guest:guest_password_here" http://localhost:8000/ +``` + +### Dynamic credentials provider + +For scenarios where credentials need to be retrieved dynamically (e.g., from a database +or LDAP), implement the `BasicCredentialsProvider` abstract class: + +```python +from typing import List +from blacksheep import Application, get +from blacksheep.server.authentication.basic import ( + BasicAuthentication, + BasicCredentials, + BasicCredentialsProvider +) +from blacksheep.server.authorization import auth +from essentials.secrets import Secret + + +class DatabaseCredentialsProvider(BasicCredentialsProvider): + """ + Example provider that retrieves credentials from a database. + """ + + def __init__(self, db_connection): + self.db = db_connection + + async def get_credentials(self) -> List[BasicCredentials]: + """ + Fetch credentials from database with associated roles and claims. + """ + # Example database query (adapt to your database) + users_data = await self.db.fetch_all(""" + SELECT username, password_hash, roles, department, access_level + FROM users + WHERE is_active = true + """) + + credentials = [] + for row in users_data: + # TODO: return a custom subclass of BasicCredentials that overrides the + # `match` method to handle the password_hash (as the client will send a + # password in clear text!) + credentials.append(BasicCredentials( + username=row["username"], + password=Secret(row["password_hash"], direct_value=True), + roles=row["roles"].split(",") if row["roles"] else [], + claims={ + "department": row["department"], + "access_level": row["access_level"] + } + )) + + return credentials + + +# Usage with dynamic provider +app = Application() + +# Assume you have a database connection +# db_connection = get_database_connection() + +app.use_authentication().add( + BasicAuthentication( + credentials_provider=DatabaseCredentialsProvider(db_connection) + ) +) + +app.use_authorization() + + +@auth() +@get("/") +async def protected_endpoint(request): + return { + "message": "Access granted", + "username": request.user.claims.get("sub"), + "department": request.user.claims.get("department"), + "access_level": request.user.claims.get("access_level") + } +``` + +**Note:** dependency injection is also supported, configuring the authentication handler as a _type_ to be instantiated rather than an instance. + +### Generating authorization headers + +The `BasicCredentials` class provides a utility method to generate the Authorization header value: + +```python +from blacksheep.server.authentication.basic import BasicCredentials +from essentials.secrets import Secret + +# Create credentials +admin_credentials = BasicCredentials( + username="admin", + password=Secret("secret_password", direct_value=True) +) + +# Generate the Authorization header value +header_value = admin_credentials.to_header_value() +print(header_value) # Output: Basic YWRtaW46c2VjcmV0X3Bhc3N3b3Jk + +# Use in HTTP client +import httpx + +response = httpx.get( + "http://localhost:8000/protected", + headers={"Authorization": header_value} +) +``` + +### Advanced configuration + +You can customize the authentication scheme: + +```python +app.use_authentication().add( + BasicAuthentication( + BasicCredentials( + username="service", + password=Secret("$SERVICE_PASSWORD"), + roles=["service"], + claims={"client_type": "internal_service"} + ), + scheme="InternalBasic", # Custom scheme name + description="Internal service authentication using Basic auth" + ) +) +``` + +/// admonition | Security recommendations + type: warning + +When implementing Basic authentication: + +- **Always use HTTPS** in production to protect credentials in transit. +- Use strong, unique passwords and consider password policies. +- If you store password in a database, store hashes with salt, not plain text passwords. + If you work with hashes and salts, define a subclass of `BasicCredentials` that + overrides the `match` method to handle hashes according to your preference. + +/// + ## OIDC BlackSheep implements built-in support for OpenID Connect authentication, From 762a5aa7338039e74d81be306159c4c936ff3436 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Sun, 5 Oct 2025 10:40:31 +0200 Subject: [PATCH 05/14] Update authentication.md --- blacksheep/docs/authentication.md | 206 ++++++++++++++++++++++++++---- 1 file changed, 184 insertions(+), 22 deletions(-) diff --git a/blacksheep/docs/authentication.md b/blacksheep/docs/authentication.md index 6e12c62..9104817 100644 --- a/blacksheep/docs/authentication.md +++ b/blacksheep/docs/authentication.md @@ -8,10 +8,19 @@ covers: - [X] How to use the built-in authentication strategy. - [X] How to configure a custom authentication handler. - [X] How to use the built-in support for **API Key** authentication. -- [X] How to use the built-in support for **Basic authentication**. +- [X] How to use the built-in support for **Basic** authentication. - [X] How to use the built-in support for **JWT Bearer** authentication. +- [X] How to use the built-in support for **Cookie** authentication. - [X] How to read the user's context in request handlers. +/// admonition | Additional dependencies. + type: warning + +Using JWT Bearer and OpenID integrations requires additional dependencies. +Install them by running: `pip install blacksheep[full]`. + +/// + ## How to use built-in authentication Common strategies for identifying users in web applications include: @@ -510,6 +519,19 @@ When implementing Basic authentication: /// +## Cookie + +BlackSheep implements a built-in class for Cookie authentication. This class can be +used to authenticate users based on a cookie, and it is used internally by default with +the OIDC integration (after a user successfully signs-in with an external identity +provider, the user context is stored in a cookie by default). + +It can set cookies and validates them automatically. + +```python + +``` + ## OIDC BlackSheep implements built-in support for OpenID Connect authentication, @@ -576,6 +598,8 @@ For more information and examples, refer to the dedicated page about ## JWT Bearer +### With Asymmetric Encryption + BlackSheep implements built-in support for JWT Bearer authentication, and validation of JWTs: @@ -633,9 +657,8 @@ async def open(user: User | None): ``` -The built-in handler for JWT Bearer authentication does not currently support -JWTs signed with symmetric keys. Support for symmetric keys might be added in -the future. +The built-in handler for JWT Bearer authentication also supports symmetric encryption, +but only since version `2.4.2`. /// admonition | 💡 @@ -646,6 +669,156 @@ Active Directory, Azure Active Directory B2C. /// +### With Symmetric Encryption + +Since version `2.4.2`, BlackSheep supports JWT Bearer authentication with symmetric +encryption using shared secret keys. This is useful for scenarios where you control both +the token issuer and validator, such as internal services or microservices +architectures. + +The following example shows how to configure JWT Bearer authentication with a symmetric +secret key: + +```python +from blacksheep import Application, get, json +from blacksheep.server.authentication.jwt import JWTBearerAuthentication +from blacksheep.server.authorization import auth +from essentials.secrets import Secret + +app = Application() + +app.use_authentication().add( + JWTBearerAuthentication( + secret_key=Secret("$JWT_SECRET"), # ⟵ obtained from JWT_SECRET env var + valid_audiences=["my-service"], + valid_issuers=["my-issuer"], + algorithms=["HS256"], # ⟵ symmetric algorithms: HS256, HS384, HS512 + auth_mode="JWT Symmetric" + ) +) + +app.use_authorization() + + +@auth() +@get("/protected") +async def protected_endpoint(request): + return { + "message": "Access granted", + "user": request.user.claims.get("sub"), + "roles": request.user.claims.get("roles", []) + } +``` + +#### Supported symmetric algorithms + +When using symmetric encryption, the following algorithms are supported: + +- `HS256` (HMAC using SHA-256) - **recommended** +- `HS384` (HMAC using SHA-384) +- `HS512` (HMAC using SHA-512) + +#### Creating symmetric JWTs + +You can create JWTs for testing using Python's `PyJWT` library: + +```python +import jwt +from datetime import datetime, timedelta + +# Your shared secret (same as in the authentication config) +secret = "your-secret-key-here" + +# Create a JWT payload +payload = { + "sub": "user123", + "aud": "my-service", + "iss": "my-issuer", + "exp": datetime.utcnow() + timedelta(hours=1), + "iat": datetime.utcnow(), + "roles": ["user", "admin"] +} + +# Generate the token +token = jwt.encode(payload, secret, algorithm="HS256") +print(f"Token: {token}") + +# Test with curl +# curl -H "Authorization: Bearer {token}" http://localhost:8000/protected +``` + +#### Multiple JWT configurations + +You can configure both symmetric and asymmetric JWT authentication handlers in the same +application to support different token types: + +```python +from blacksheep import Application +from blacksheep.server.authentication.jwt import JWTBearerAuthentication +from essentials.secrets import Secret + +app = Application() + +# Symmetric JWT for internal services +app.use_authentication().add( + JWTBearerAuthentication( + secret_key=Secret("$INTERNAL_JWT_SECRET"), + valid_audiences=["internal-api"], + valid_issuers=["internal-issuer"], + algorithms=["HS256"], + auth_mode="JWT Internal" + ) +) + +# Asymmetric JWT for external identity providers +app.use_authentication().add( + JWTBearerAuthentication( + authority="https://login.microsoftonline.com/tenant.onmicrosoft.com", + valid_audiences=["external-client-id"], + valid_issuers=["https://login.microsoftonline.com/tenant-id/v2.0"], + algorithms=["RS256"], + auth_mode="JWT External" + ) +) +``` + +/// admonition | Security considerations + type: warning + +When using symmetric JWT authentication: + +- **Use strong secret keys**: Generate cryptographically secure random keys of at least + 256 bits for HS256. +- **Protect your secrets**: Store secret keys securely and never commit them to version + control. +- **Key rotation**: Implement a strategy for rotating secret keys periodically. +- **Secure transmission**: Always use HTTPS in production to protect tokens in transit. +- **Token expiration**: Set appropriate expiration times (`exp` claim) for your tokens. + +/// + +/// admonition | Symmetric vs Asymmetric + type: info + +**Symmetric encryption** (shared secret): +- ✅ Faster validation (no key fetching required) +- ✅ Simpler setup for internal services +- ❌ Same key used for signing and validation +- ❌ Key distribution challenges in distributed systems + +**Asymmetric encryption** (public/private keys): +- ✅ Better security model (separate keys for signing/validation) +- ✅ Better for third-party integrations +- ❌ Slower validation (key fetching and cryptographic operations) +- ❌ More complex setup + +Choose symmetric encryption for internal services where you control both token creation +and validation. Use asymmetric encryption when integrating with external identity +providers or when you need to distribute validation capabilities without sharing signing +keys. + +/// + ## Writing a custom authentication handler The example below shows how to configure a custom authentication handler that @@ -884,23 +1057,6 @@ async def optional_auth_endpoint(request): return {"message": "Anonymous user"} ``` -## Next - -While authentication focuses on *identifying* users, authorization determines -whether a user *is permitted* to perform the requested action. The next page -describes the built-in [authorization strategy](authorization.md) in -BlackSheep. - - + +## Next + +While authentication focuses on *identifying* users, authorization determines +whether a user *is permitted* to perform the requested action. The next page +describes the built-in [authorization strategy](authorization.md) in +BlackSheep. From e961a63d24316fdc9a4e7c9dbcd1b7166e8a3168 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Sun, 5 Oct 2025 10:44:17 +0200 Subject: [PATCH 06/14] Update authentication.md --- blacksheep/docs/authentication.md | 205 +++++++++++++++++++++++++++++- 1 file changed, 204 insertions(+), 1 deletion(-) diff --git a/blacksheep/docs/authentication.md b/blacksheep/docs/authentication.md index 9104817..29318d7 100644 --- a/blacksheep/docs/authentication.md +++ b/blacksheep/docs/authentication.md @@ -526,12 +526,215 @@ used to authenticate users based on a cookie, and it is used internally by defau the OIDC integration (after a user successfully signs-in with an external identity provider, the user context is stored in a cookie by default). -It can set cookies and validates them automatically. +Cookie authentication automatically handles setting, validating, and unsetting cookies +with signed and encrypted user data using `itsdangerous.Serializer`. + +### Basic Cookie authentication setup + +The following example shows how to configure Cookie authentication: + +```python +from blacksheep import Application, get, json +from blacksheep.server.authentication.cookie import CookieAuthentication +from blacksheep.server.authorization import auth + +app = Application() + +# Configure cookie authentication +app.use_authentication().add( + CookieAuthentication( + cookie_name="user_session", # Default: "identity" + secret_keys=["your-secret-key"], # Keys for signing/encryption + auth_scheme="CookieAuth" # Custom scheme name + ) +) + +app.use_authorization() + + +@auth() +@get("/profile") +async def get_profile(request): + return { + "message": "User profile", + "user": request.user.claims + } + + +@get("/login") +async def login(request): + """Example login endpoint that sets authentication cookie""" + response = json({"message": "Login successful"}) + + # Get the cookie authentication handler + cookie_auth = app.services.resolve(CookieAuthentication) + + # Set user data in cookie (typically done after validating credentials) + user_data = { + "sub": "user123", + "name": "John Doe", + "roles": ["user"], + "exp": 1234567890 # Optional expiration timestamp + } + + cookie_auth.set_cookie(user_data, response, secure=True) + return response + + +@get("/logout") +async def logout(request): + """Example logout endpoint that removes authentication cookie""" + response = json({"message": "Logged out"}) + + # Get the cookie authentication handler + cookie_auth = app.services.resolve(CookieAuthentication) + + # Remove the authentication cookie + cookie_auth.unset_cookie(response) + return response +``` + +### Advanced Cookie configuration + +You can customize the cookie authentication with additional options: + +```python +from blacksheep import Application +from blacksheep.server.authentication.cookie import CookieAuthentication +from itsdangerous import JSONWebSignatureSerializer + +app = Application() + +# Advanced configuration with custom serializer +custom_serializer = JSONWebSignatureSerializer("your-secret-key") + +app.use_authentication().add( + CookieAuthentication( + cookie_name="app_session", + secret_keys=["primary-key", "backup-key"], # Key rotation support + serializer=custom_serializer, # Custom serializer + auth_scheme="CustomCookieAuth" + ) +) +``` + +### Working with cookie data + +The cookie authentication handler provides methods to manage authentication cookies: + +```python +from blacksheep import Application, get, post, json +from blacksheep.server.authentication.cookie import CookieAuthentication + +app = Application() + +cookie_auth = CookieAuthentication( + cookie_name="session", + secret_keys=["your-secret-key"] +) + +app.use_authentication().add(cookie_auth) + + +@post("/api/signin") +async def signin(request): + """Sign in endpoint that validates credentials and sets cookie""" + # TODO: Validate user credentials from request body + + response = json({"success": True}) + + # Set authentication cookie with user claims + user_claims = { + "sub": "user123", + "email": "user@example.com", + "roles": ["user", "admin"], + "department": "IT" + } + + cookie_auth.set_cookie(user_claims, response, secure=True) + return response + + +@post("/api/signout") +async def signout(request): + """Sign out endpoint that removes the authentication cookie""" + response = json({"message": "Signed out successfully"}) + cookie_auth.unset_cookie(response) + return response + + +@get("/api/user") +async def get_current_user(request): + """Get current user info from cookie authentication""" + if request.user and request.user.is_authenticated(): + return json({ + "authenticated": True, + "claims": request.user.claims + }) + else: + return json({"authenticated": False}) +``` + +### Cookie security considerations + +When using cookie authentication, consider these security practices: ```python +from blacksheep import Application +from blacksheep.server.authentication.cookie import CookieAuthentication +from datetime import datetime, timedelta + +app = Application() + +# Secure cookie configuration +app.use_authentication().add( + CookieAuthentication( + cookie_name="secure_session", + secret_keys=[ + "primary-secret-key-256-bits-long", + "backup-secret-key-for-rotation" + ] + ) +) + + +@app.route("/login", methods=["POST"]) +async def secure_login(request): + # TODO: Validate credentials + response = json({"success": True}) + + # Set cookie with expiration + user_data = { + "sub": "user123", + "name": "John Doe", + "exp": int((datetime.utcnow() + timedelta(hours=24)).timestamp()) + } + + cookie_auth = app.services.resolve(CookieAuthentication) + cookie_auth.set_cookie( + user_data, + response, + secure=True # Always use secure=True in production with HTTPS + ) + return response ``` +/// admonition | Security recommendations + type: warning + +When implementing Cookie authentication: + +- **Do not** hard-code secrets in source code. The examples above are just **examples**. +- **Use strong secret keys**: Generate cryptographically secure random keys. +- **Enable secure flag**: Always set `secure=True` when using HTTPS in production. +- **Key rotation**: Use multiple secret keys to support key rotation without breaking + existing sessions. +- **Set expiration**: Include `exp` claim in cookie data to control session lifetime. +- **Use HTTPS**: Never transmit authentication cookies over unencrypted connections. + +/// + ## OIDC BlackSheep implements built-in support for OpenID Connect authentication, From 98d7d7069afb3d643cbda82df0dd718dc86c6866 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Sun, 5 Oct 2025 12:18:03 +0200 Subject: [PATCH 07/14] Update authentication.md --- blacksheep/docs/authentication.md | 58 +++++++++++++++---------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/blacksheep/docs/authentication.md b/blacksheep/docs/authentication.md index 29318d7..09c658e 100644 --- a/blacksheep/docs/authentication.md +++ b/blacksheep/docs/authentication.md @@ -12,6 +12,7 @@ covers: - [X] How to use the built-in support for **JWT Bearer** authentication. - [X] How to use the built-in support for **Cookie** authentication. - [X] How to read the user's context in request handlers. +- [X] How authentication can be documented in **OpenAPI Documentation**. /// admonition | Additional dependencies. type: warning @@ -25,26 +26,21 @@ Install them by running: `pip install blacksheep[full]`. Common strategies for identifying users in web applications include: -- Reading an `Authorization: Bearer xxx` request header containing a [JWT](https://jwt.io/introduction/). - with claims that identify the user. - Reading a signed token from a cookie. +- Handling API Keys sent in custom headers. +- Handling basic credentials sent in `Authorization: Basic ***` headers. +- Handling JSON Web Tokens (JWTs) signed and including payloads with information + about the user, transmitted using `Authorization: Bearer ***` request headers. -The following sections first explain how to use the built-in support for JWT -Bearer tokens and then describe how to write a custom authentication handler. - -/// admonition | Terms: user, service, principal. - -The term 'user' typically refers to human users, while 'service' describes -non-human clients. In Java and .NET, the term 'principal' is commonly used to -describe a generic identity. - -/// +The following sections describe how to enable authentication using built-in +classes, and how to define custom authentication handlers. ## API Key authentication -Since version `2.4.2`, BlackSheep provides built-in support for API Key authentication -with flexible configuration options. API Keys can be read from request headers, query -parameters, or cookies, and each key can be associated with specific roles and claims. +Since version `2.4.2`, BlackSheep provides built-in support for API Key +authentication with flexible configuration options. API Keys can be read from +request headers, query parameters, or cookies, and each key can be associated +with specific roles and claims. ### Enabling API Key authentication @@ -985,31 +981,18 @@ app.use_authentication().add( ) ``` -/// admonition | Security considerations - type: warning - -When using symmetric JWT authentication: - -- **Use strong secret keys**: Generate cryptographically secure random keys of at least - 256 bits for HS256. -- **Protect your secrets**: Store secret keys securely and never commit them to version - control. -- **Key rotation**: Implement a strategy for rotating secret keys periodically. -- **Secure transmission**: Always use HTTPS in production to protect tokens in transit. -- **Token expiration**: Set appropriate expiration times (`exp` claim) for your tokens. - -/// - /// admonition | Symmetric vs Asymmetric type: info **Symmetric encryption** (shared secret): + - ✅ Faster validation (no key fetching required) - ✅ Simpler setup for internal services - ❌ Same key used for signing and validation - ❌ Key distribution challenges in distributed systems **Asymmetric encryption** (public/private keys): + - ✅ Better security model (separate keys for signing/validation) - ✅ Better for third-party integrations - ❌ Slower validation (key fetching and cryptographic operations) @@ -1022,6 +1005,21 @@ keys. /// +/// admonition | Security considerations + type: warning + +When using symmetric JWT authentication: + +- **Use strong secret keys**: Generate cryptographically secure random keys of at least + 256 bits for HS256. +- **Protect your secrets**: Store secret keys securely and never commit them to version + control. +- **Key rotation**: Implement a strategy for rotating secret keys periodically. +- **Secure transmission**: Always use HTTPS in production to protect tokens in transit. +- **Token expiration**: Set appropriate expiration times (`exp` claim) for your tokens. + +/// + ## Writing a custom authentication handler The example below shows how to configure a custom authentication handler that From 879410ef1d9c2c77c1cbaa8273b2c0a2d3197cf3 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Sun, 5 Oct 2025 17:50:55 +0200 Subject: [PATCH 08/14] Update --- blacksheep/docs/authentication.md | 8 ++++---- blacksheep/docs/opentelemetry.md | 25 ++++++++++++++++++------- blacksheep/mkdocs.yml | 1 + requirements.txt | 1 + 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/blacksheep/docs/authentication.md b/blacksheep/docs/authentication.md index 09c658e..b40ae98 100644 --- a/blacksheep/docs/authentication.md +++ b/blacksheep/docs/authentication.md @@ -722,7 +722,8 @@ async def secure_login(request): When implementing Cookie authentication: - **Do not** hard-code secrets in source code. The examples above are just **examples**. -- **Use strong secret keys**: Generate cryptographically secure random keys. +- **Use strong secret keys**: Generate cryptographically secure random keys, + for example using `secrets.choice`. - **Enable secure flag**: Always set `secure=True` when using HTTPS in production. - **Key rotation**: Use multiple secret keys to support key rotation without breaking existing sessions. @@ -1204,8 +1205,8 @@ def home(request: Request): type: tip As documented in [_Container Protocol_](./dependency-injection.md#the-container-protocol), -BlackSheep supports the use of other DI containers as replacements for the built-in -library used for dependency injection. +BlackSheep supports the use of other DI containers as replacements for the +built-in library used for dependency injection. /// @@ -1265,7 +1266,6 @@ published in a dedicated library: [`guardpost`](https://github.com/neoteroi/guardpost) ([in pypi](https://pypi.org/project/guardpost/)). - ## Next While authentication focuses on *identifying* users, authorization determines diff --git a/blacksheep/docs/opentelemetry.md b/blacksheep/docs/opentelemetry.md index 45a27cd..35030b5 100644 --- a/blacksheep/docs/opentelemetry.md +++ b/blacksheep/docs/opentelemetry.md @@ -19,6 +19,17 @@ automatically trace incoming requests, outgoing HTTP calls, and other operations, providing end-to-end visibility into your application's execution flow. +/// note | Tutorial on self-hosted monitoring stack. + +For a tutorial on how to self-host a full monitoring stack in a single node of +a Kubernetes cluster (which is fine for non-production environments), and to +know more about OpenTelemetry and the OTLP protocol, you can read: +[_K8s Studies on Monitoring_](https://robertoprevato.github.io/K8sStudies/k3s/monitoring/). +It includes a working example of BlackSheep application that sends telemetries +to an OpenTelemetry Collector. + +/// + ## Enabling OpenTelemetry in BlackSheep BlackSheep offers built-in support for OpenTelemetry since version `2.3.2`, but @@ -155,9 +166,9 @@ A trace is produced for each web request and for all handled and unhandled exceptions. For unhandled exceptions, OpenTelemetry includes the full stacktrace of the exception. -[![Grafana traces](./img/grafana-traces.png)](./img/grafana-traces.png) +![Grafana traces](./img/grafana-traces.png) -[![Grafana errors](./img/grafana-errors.png)](./img/grafana-errors.png) +![Grafana errors](./img/grafana-errors.png) ## Example: Azure Application Insights @@ -205,7 +216,7 @@ use_application_insights(app, "YOUR_CONN_STRING") Observe how web requests and errors are displayed: -[![Azure Application Insights Requests](./img/azure-app-insights-requests.png)](./img/azure-app-insights-requests.png) +![Azure Application Insights Requests](./img/azure-app-insights-requests.png) ## Logging dependencies @@ -233,9 +244,9 @@ async def home(request) -> Response: The following screenshots illustrate how dependencies are displayed in Grafana and Azure Application Insights: -[![Dependency in Grafana](./img/grafana-dependency.png)](./img/grafana-dependency.png) +![Dependency in Grafana](./img/grafana-dependency.png) -[![Dependency in Azure Application Insights](./img/azure-app-insights-dependency.png)](./img/azure-app-insights-dependency.png) +![Dependency in Azure Application Insights](./img/azure-app-insights-dependency.png) /// admonition | Dependencies with Azure Application Insights. type: example @@ -294,7 +305,7 @@ async def home(request) -> Response: logger.warning("Example warning") ``` -[![Logs sent to backend](./img/grafana-logs.png)](./img/grafana-logs.png) +![Logs sent to backend](./img/grafana-logs.png) To filter by a different priority, set a different level on the root logger: @@ -305,7 +316,7 @@ import logging logging.getLogger().setLevel(logging.INFO) ``` -[![Logs sent to backend](./img/grafana-logs-info.png)](./img/grafana-logs-info.png) +![Logs sent to backend](./img/grafana-logs-info.png) ## Service information diff --git a/blacksheep/mkdocs.yml b/blacksheep/mkdocs.yml index 938987b..6907e3b 100644 --- a/blacksheep/mkdocs.yml +++ b/blacksheep/mkdocs.yml @@ -90,6 +90,7 @@ extra_javascript: plugins: - search + - glightbox - neoteroi.contribs: enabled_by_env: "GIT_CONTRIBS_ON" diff --git a/requirements.txt b/requirements.txt index a6a7f7f..8f4275a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ mkdocs-material==9.6.9 neoteroi-mkdocs>=1.1.1 +mkdocs-glightbox==0.5.1 From 580b3a47c7fdf381cc30f4a47827cb60c9b3d142 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Sun, 5 Oct 2025 22:30:28 +0200 Subject: [PATCH 09/14] Update authentication.md --- blacksheep/docs/authentication.md | 304 ++++++++++++++++++++++++++++++ 1 file changed, 304 insertions(+) diff --git a/blacksheep/docs/authentication.md b/blacksheep/docs/authentication.md index b40ae98..52f70e5 100644 --- a/blacksheep/docs/authentication.md +++ b/blacksheep/docs/authentication.md @@ -1266,6 +1266,310 @@ published in a dedicated library: [`guardpost`](https://github.com/neoteroi/guardpost) ([in pypi](https://pypi.org/project/guardpost/)). +## Documenting authentication + +Since version `2.4.2`, BlackSheep automatically generates OpenAPI Documentation for +authentication handlers when using the built-in authentication classes. This means +that when you configure API Key, Basic, or JWT Bearer the +corresponding security schemes are automatically added to your OpenAPI specification. + +### Automatic documentation for built-in handlers + +The following built-in authentication handlers are automatically documented: + +- **APIKeyAuthentication** → generates `ApiKey` security scheme. +- **BasicAuthentication** → generates `HTTP Basic` security scheme. +- **JWTBearerAuthentication** → generates `HTTP Bearer` security scheme with JWT format. + +```python +from blacksheep import Application +from blacksheep.server.authentication.apikey import APIKey, APIKeyAuthentication +from blacksheep.server.authentication.basic import BasicAuthentication, BasicCredentials +from blacksheep.server.authentication.jwt import JWTBearerAuthentication +from blacksheep.server.openapi.v3 import OpenAPIHandler +from openapidocs.v3 import Info +from essentials.secrets import Secret + +app = Application() + +# Configure OpenAPI documentation +docs = OpenAPIHandler(info=Info(title="My API", version="1.0.0")) +docs.bind_app(app) + +# These authentication handlers will be automatically documented +app.use_authentication().add( + APIKeyAuthentication( + APIKey(secret=Secret("$API_SECRET")), + param_name="X-API-Key", + description="API key for authentication" # ⟵ appears in OpenAPI docs + ) +).add( + BasicAuthentication( + BasicCredentials( + username="admin", + password=Secret("$ADMIN_PASSWORD") + ), + description="Basic authentication for admin users" # ⟵ appears in OpenAPI docs + ) +).add( + JWTBearerAuthentication( + authority="https://your-authority.com", + valid_audiences=["your-audience"] + # Automatically documented as Bearer JWT + ) +) +``` + +The generated OpenAPI specification will include: + +```yaml +components: + securitySchemes: + ApiKey: + type: apiKey + name: X-API-Key + in: header + description: API key for authentication + Basic: + type: http + scheme: basic + description: Basic authentication for admin users + Bearer: + type: http + scheme: bearer + bearerFormat: JWT +security: + - ApiKey: [] + - Basic: [] + - JWTBearerAuthentication: [] +``` + +### Custom SecuritySchemeHandler + +To control how your custom authentication handlers are documented, implement the +`SecuritySchemeHandler` abstract class: + +```python +from typing import Iterable, Tuple +from blacksheep import Application +from blacksheep.server.openapi.v3 import OpenAPIHandler, SecuritySchemeHandler +from blacksheep.server.authentication import AuthenticationHandler +from guardpost import AuthenticationHandler, Identity +from openapidocs.v3 import Info, SecurityScheme, HTTPSecurity, SecurityRequirement + +# Custom authentication handler +class CustomTokenAuthentication(AuthenticationHandler): + def __init__(self, scheme: str = "CustomToken"): + self.scheme = scheme + self.description = "Custom token authentication" + + async def authenticate(self, context) -> Identity | None: + # Your custom authentication logic here + token = context.get_first_header(b"X-Custom-Token") + if token: + # Validate token and return identity + return Identity({"sub": "user123"}, self.scheme) + return None + + +# Custom security scheme handler +class CustomTokenSecuritySchemeHandler(SecuritySchemeHandler): + def get_security_schemes( + self, handler: AuthenticationHandler + ) -> Iterable[Tuple[str, SecurityScheme, SecurityRequirement]]: + if isinstance(handler, CustomTokenAuthentication): + yield handler.scheme, HTTPSecurity( + scheme="bearer", + bearer_format="CustomToken", + description=handler.description + ), SecurityRequirement(handler.scheme, []) + + +app = Application() + +# Configure OpenAPI with custom security scheme handler +docs = OpenAPIHandler(info=Info(title="My API", version="1.0.0")) +docs.security_schemes_handlers.append(CustomTokenSecuritySchemeHandler()) +docs.bind_app(app) + +# Configure authentication +app.use_authentication().add(CustomTokenAuthentication()) +``` + +### Multiple authentication methods + +When you configure multiple authentication handlers, they are all documented and +the OpenAPI specification allows clients to choose any of the supported methods: + +```python +from blacksheep import Application, get +from blacksheep.server.authentication.apikey import APIKey, APIKeyAuthentication +from blacksheep.server.authentication.jwt import JWTBearerAuthentication +from blacksheep.server.authorization import auth +from blacksheep.server.openapi.v3 import OpenAPIHandler +from openapidocs.v3 import Info +from essentials.secrets import Secret + +app = Application() + +docs = OpenAPIHandler(info=Info(title="Multi-Auth API", version="1.0.0")) +docs.bind_app(app) + +# Configure multiple authentication methods +app.use_authentication().add( + # API Key authentication + APIKeyAuthentication( + APIKey(secret=Secret("$API_SECRET")), + param_name="X-API-Key", + description="API key authentication" + ) +).add( + # JWT Bearer authentication + JWTBearerAuthentication( + authority="https://your-authority.com", + valid_audiences=["your-audience"] + ) +) + +app.use_authorization() + + +@auth() # Accepts either API Key or JWT Bearer +@get("/protected") +async def protected_endpoint(): + return {"message": "Authenticated successfully"} +``` + +This generates OpenAPI documentation that shows both authentication methods are +supported, and clients can use either one. + +### Endpoint-specific authentication requirements + +You can document different authentication requirements for different endpoints: + +```python +from blacksheep import Application, get +from blacksheep.server.authentication.apikey import APIKey, APIKeyAuthentication +from blacksheep.server.authentication.basic import BasicAuthentication, BasicCredentials +from blacksheep.server.authentication.jwt import JWTBearerAuthentication +from blacksheep.server.authorization import allow_anonymous, auth +from blacksheep.server.openapi.common import SecurityInfo +from blacksheep.server.openapi.v3 import OpenAPIHandler, Info +from essentials.secrets import Secret + + +app = Application() + + +app.use_authentication().add( + APIKeyAuthentication( + APIKey( + secret=Secret("$API_SECRET"), # Obtained from API_SECRET env var + roles=["user"], + ), + param_name="X-API-Key", + ) +).add( + BasicAuthentication( + BasicCredentials( + username="admin", + password=Secret("$ADMIN_PASSWORD"), # Obtained from ADMIN_PASSWORD env var + roles=["admin"], + ) + ) +).add( + JWTBearerAuthentication( + valid_audiences=["myaudience"], + valid_issuers=["myapp"], + secret_key=Secret("$JWT_SECRET"), # Obtained from JWT_SECRET env var + ) +) + +app.use_authorization() + + +# See the generated docs and how they include security sections +docs = OpenAPIHandler(info=Info(title="Example API", version="0.0.1")) +docs.bind_app(app) + + +@auth(authentication_schemes=["ApiKey"]) # Only API Key authentication +@docs(security=[SecurityInfo("ApiKey", [])]) +@get("/api-key-only") +async def api_key_only(): + return {"message": "API Key required"} + + +@auth(authentication_schemes=["Bearer"]) # Only JWT Bearer authentication +@docs(security=[SecurityInfo("Bearer", [])]) +@get("/jwt-only") +async def jwt_only(): + return {"message": "JWT Bearer required"} + + +@allow_anonymous() +@get("/public") +async def public_endpoint(): + return {"message": "Public endpoint"} +``` + +### Legacy approach (before v2.4.2) + +Before version `2.4.2`, users needed to manually configure security schemes using +the `on_docs_created` event handler: + +```python +from blacksheep import Application +from blacksheep.server.openapi.v3 import OpenAPIHandler +from openapidocs.v3 import Info, APIKeySecurity, ParameterLocation, SecurityRequirement + +app = Application() + +docs = OpenAPIHandler(info=Info(title="My API", version="1.0.0")) + + +# Legacy approach - manual security scheme configuration +@docs.events.on_docs_created +def configure_security_schemes(openapi_docs): + # Manually add security schemes + if openapi_docs.components is None: + openapi_docs.components = Components() + if openapi_docs.components.security_schemes is None: + openapi_docs.components.security_schemes = {} + + # Add API Key security scheme + openapi_docs.components.security_schemes["ApiKey"] = APIKeySecurity( + name="X-API-Key", + in_=ParameterLocation.HEADER, + description="API key for authentication" + ) + + # Set global security requirement + openapi_docs.security = [SecurityRequirement("ApiKey", [])] + + +docs.bind_app(app) + +# Configure your custom authentication handler +# app.use_authentication().add(YourCustomHandler()) +``` + +/// admonition | Recommendation. + type: tip + +**Use built-in handlers when possible**: The built-in authentication handlers +(APIKeyAuthentication, BasicAuthentication, JWTBearerAuthentication) provide +automatic OpenAPI documentation and are well-tested. + +**For custom handlers**: Implement a `SecuritySchemeHandler` to ensure your +authentication methods are properly documented in the OpenAPI specification. + +**Migration from legacy approach**: If you're upgrading from a version before 2.4.2, +consider replacing manual security scheme configuration with built-in handlers or +custom SecuritySchemeHandler implementations. + +/// + ## Next While authentication focuses on *identifying* users, authorization determines From b5338838dcffc5c77e0e960247206f99b099e8d6 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Sun, 5 Oct 2025 22:33:59 +0200 Subject: [PATCH 10/14] Update binders.md --- blacksheep/docs/binders.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/blacksheep/docs/binders.md b/blacksheep/docs/binders.md index 579e7bf..e75f97a 100644 --- a/blacksheep/docs/binders.md +++ b/blacksheep/docs/binders.md @@ -1,4 +1,5 @@ # Binders + BlackSheep implements automatic binding of parameters for request handlers, a feature inspired by "Model Binding" in the [ASP.NET web framework](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-2.2). @@ -71,6 +72,7 @@ from `BoundValue` class, defined in `blacksheep.server.bindings`. the `Request` of the web request. ### Explicit binding + Binders can be defined explicitly, using type annotations and classes from `blacksheep.server.bindings` (or just `blacksheep`). @@ -115,6 +117,7 @@ extra unused properties, use `*args` in your class constructor: `__init__(one, two, three, *args)`. ## Optional parameters + Optional parameters can be defined in one of these ways: 1. using `typing.Optional` annotation @@ -273,3 +276,12 @@ def home(something: FromCustomValue): return f"OK {something.value}" ``` + + + +## Type Converters + + From 4dd9500420c3af1c8df69f7599a8a597da8a3f4a Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Sun, 5 Oct 2025 22:53:28 +0200 Subject: [PATCH 11/14] Update binders.md --- blacksheep/docs/binders.md | 372 ++++++++++++++++++++++++++++++++++++- 1 file changed, 367 insertions(+), 5 deletions(-) diff --git a/blacksheep/docs/binders.md b/blacksheep/docs/binders.md index e75f97a..c00a9ce 100644 --- a/blacksheep/docs/binders.md +++ b/blacksheep/docs/binders.md @@ -277,11 +277,373 @@ def home(something: FromCustomValue): ``` - +## Custom Convert Functions in BoundValue Classes + +Since version `2.4.1`, custom `BoundValue` classes can define a `convert` class method to transform Python objects from parsed JSON into more specific types. This is particularly useful when you need to apply custom validation or transformation logic during the binding process. + +### Defining a Convert Function + +To add custom conversion logic to a `BoundValue` class, define a `convert` class method: + +```python +from typing import Any, Dict +from blacksheep import Application, FromJSON, post +from blacksheep.server.bindings import BoundValue + +class CustomData(BoundValue[Dict[str, Any]]): + """ + Custom bound value with conversion logic. + """ + + @classmethod + def convert(cls, value: Any) -> Dict[str, Any]: + """ + Convert the parsed JSON value into the desired format. + This method is called after JSON parsing but before creating the BoundValue instance. + """ + if isinstance(value, dict): + # Apply custom validation and transformation + if 'required_field' not in value: + raise ValueError("Missing required_field in request data") + + # Transform the data + return { + 'processed': True, + 'original': value, + 'timestamp': value.get('timestamp', 'default_value') + } + + raise ValueError("Expected a dictionary object") + +app = Application() + +@post("/api/data") +async def process_data(data: FromJSON[CustomData]): + # data.value contains the converted dictionary + return { + "received": data.value, + "processed": data.value['processed'] + } +``` + +### Advanced Custom Conversion + +You can implement more complex conversion logic for specific use cases: + +```python +from dataclasses import dataclass +from datetime import datetime +from typing import Optional +from blacksheep import FromJSON, post +from blacksheep.server.bindings import BoundValue + +@dataclass +class UserProfile: + name: str + email: str + created_at: datetime + age: Optional[int] = None + +class UserProfileBinder(BoundValue[UserProfile]): + """ + Custom binder that converts JSON to UserProfile with date parsing. + """ + + @classmethod + def convert(cls, value: Any) -> UserProfile: + if not isinstance(value, dict): + raise ValueError("Expected a dictionary for UserProfile") + + # Parse the datetime string + created_at_str = value.get('created_at') + if isinstance(created_at_str, str): + try: + created_at = datetime.fromisoformat(created_at_str) + except ValueError: + raise ValueError("Invalid datetime format for created_at") + else: + created_at = datetime.utcnow() + + # Validate required fields + if not value.get('name') or not value.get('email'): + raise ValueError("Name and email are required fields") + + return UserProfile( + name=value['name'], + email=value['email'], + created_at=created_at, + age=value.get('age') + ) + +@post("/api/users") +async def create_user(profile: FromJSON[UserProfileBinder]): + user = profile.value # This is a UserProfile instance + return { + "message": f"User {user.name} created successfully", + "user_id": hash(user.email), + "created_at": user.created_at.isoformat() + } +``` + +### Error Handling in Convert Functions + +Convert functions should raise appropriate exceptions for invalid data: + +```python +from blacksheep.server.bindings import BoundValue +from blacksheep.exceptions import BadRequest + +class ValidatedInput(BoundValue[dict]): + @classmethod + def convert(cls, value: Any) -> dict: + if not isinstance(value, dict): + raise BadRequest("Expected JSON object") + + # Custom validation logic + if 'id' not in value: + raise BadRequest("Missing 'id' field") + + if not isinstance(value['id'], int) or value['id'] <= 0: + raise BadRequest("Field 'id' must be a positive integer") + + return value +``` + +When the `convert` method raises an exception, BlackSheep automatically returns a `400 Bad Request` response with the error message. + +/// admonition | Convert Method Behavior + type: info + +- The `convert` method is called **after** JSON parsing but **before** the `BoundValue` instance is created. +- It receives the parsed Python object (dict, list, etc.) as input. +- The return value becomes the `value` property of the `BoundValue` instance. +- Exceptions raised in `convert` methods are automatically converted to `400 Bad Request` responses. + +/// ## Type Converters - +Since version `2.4.1`, BlackSheep provides a flexible type conversion system through the `TypeConverter` abstract class. This system allows automatic conversion of string representations from request parameters (query, headers, route, etc.) into specific Python types. + +### Built-in Type Converters + +BlackSheep includes several built-in type converters that handle common data types: + +| Converter | Supported Types | Description | +| ------------------- | --------------- | ------------------------------------------------- | +| `StrConverter` | `str` | Handles string values with URL decoding | +| `BoolConverter` | `bool` | Converts "true"/"false", "1"/"0" to boolean | +| `IntConverter` | `int` | Converts strings to integers | +| `FloatConverter` | `float` | Converts strings to floating-point numbers | +| `UUIDConverter` | `UUID` | Converts strings to UUID objects | +| `BytesConverter` | `bytes` | Converts strings to bytes using UTF-8 encoding | +| `DateTimeConverter` | `datetime` | Parses ISO datetime strings | +| `DateConverter` | `date` | Parses ISO date strings | +| `StrEnumConverter` | `StrEnum` | Converts strings to StrEnum values (Python 3.11+) | +| `IntEnumConverter` | `IntEnum` | Converts strings to IntEnum values (Python 3.11+) | +| `LiteralConverter` | `Literal` | Validates against literal type values | + +### String Enum Support (Python 3.11+) + +BlackSheep provides automatic support for `StrEnum` types: + +```python +from enum import StrEnum +from blacksheep import Application, get + +app = Application() + +class Color(StrEnum): + RED = "red" + GREEN = "green" + BLUE = "blue" + +@get("/items") +async def get_items(color: Color): + # color parameter automatically converted to Color enum + return {"color": color.value, "name": color.name} + +# Usage examples: +# GET /items?color=red -> Color.RED +# GET /items?color=GREEN -> Color.GREEN (by name) +# GET /items?color=invalid -> 400 Bad Request +``` + +### Integer Enum Support (Python 3.11+) + +Similarly, `IntEnum` types are automatically supported: + +```python +from enum import IntEnum +from blacksheep import Application, get + +class Priority(IntEnum): + LOW = 1 + MEDIUM = 2 + HIGH = 3 + +@get("/tasks") +async def get_tasks(priority: Priority): + return {"priority": priority.value, "name": priority.name} + +# Usage examples: +# GET /tasks?priority=1 -> Priority.LOW +# GET /tasks?priority=HIGH -> Priority.HIGH (by name) +# GET /tasks?priority=5 -> 400 Bad Request +``` + +### Literal Type Support + +BlackSheep supports `typing.Literal` for restricting values to specific literals: + +```python +from typing import Literal +from blacksheep import Application, get + +@get("/api/data") +async def get_data(format: Literal["json", "xml", "csv"]): + return {"format": format, "message": f"Returning data in {format} format"} + +# Usage examples: +# GET /api/data?format=json -> format="json" +# GET /api/data?format=pdf -> 400 Bad Request + +# Case-insensitive literal matching +from blacksheep.server.bindings.converters import LiteralConverter + +# Configure case-insensitive matching (if needed globally) +# This would require custom binder configuration +``` + +### Custom Type Converter + +You can define custom type converters by implementing the `TypeConverter` abstract class: + +```python +from abc import abstractmethod +from blacksheep.server.bindings.converters import TypeConverter +from blacksheep.server.bindings.converters import converters +from blacksheep import Application, get + +# Custom type example +class ProductCode: + def __init__(self, code: str): + if not code.startswith("PROD-"): + raise ValueError("Product code must start with 'PROD-'") + if len(code) != 10: + raise ValueError("Product code must be exactly 10 characters") + self.code = code + + def __str__(self): + return self.code + +# Custom converter +class ProductCodeConverter(TypeConverter): + def can_convert(self, expected_type) -> bool: + return expected_type is ProductCode + + def convert(self, value, expected_type): + if value is None: + return None + try: + return ProductCode(value) + except ValueError as e: + raise ValueError(f"Invalid product code: {e}") + +# Register the custom converter +converters.append(ProductCodeConverter()) + +app = Application() + +@get("/products/{product_code}") +async def get_product(product_code: ProductCode): + return {"product_code": str(product_code)} + +# Usage examples: +# GET /products/PROD-12345 -> ProductCode("PROD-12345") +# GET /products/INVALID -> 400 Bad Request +``` + +### Advanced Converter Configuration + +For more complex scenarios, you can configure converters with custom options: + +```python +from blacksheep.server.bindings.converters import LiteralConverter +from blacksheep import FromQuery, get + +# Case-insensitive literal converter +case_insensitive_converter = LiteralConverter(case_insensitive=True) + +class CustomFromQuery(FromQuery[T]): + def __init__(self, default_value=None): + super().__init__(default_value) + # Custom converter logic could be added here + +@get("/search") +async def search( + sort_order: Literal["asc", "desc"] = "asc", + category: Literal["books", "movies", "games"] = "books" +): + # Both parameters support case-insensitive matching if configured + return {"sort_order": sort_order, "category": category} +``` + +### Error Handling in Type Conversion + +When type conversion fails, BlackSheep automatically returns a `400 Bad Request` response: + +```python +from enum import StrEnum +from blacksheep import Application, get + +class Status(StrEnum): + ACTIVE = "active" + INACTIVE = "inactive" + +@get("/users") +async def get_users(status: Status): + return {"status": status} + +# GET /users?status=invalid -> 400 Bad Request with message: +# "invalid is not a valid Status" +``` + +### Type Converter Priority + +Converters are evaluated in the order they appear in the `converters` list. Built-in converters are registered by default, and custom converters are typically appended to the list. + +```python +from blacksheep.server.bindings.converters import converters + +# View all registered converters +for converter in converters: + print(f"{converter.__class__.__name__}: {converter}") + +# Add custom converter with priority (insert at beginning) +converters.insert(0, YourCustomConverter()) +``` + +/// admonition | Version Requirements + type: info + +- **StrEnum and IntEnum support**: Requires Python 3.11 or later +- **Literal support**: Available in all supported Python versions (3.8+) +- **Custom TypeConverter**: Available since BlackSheep 2.4.1 + +For older Python versions, you can achieve similar functionality using regular `Enum` classes or custom validation in your request handlers. + +/// + +/// admonition | Best Practices + type: tip + +1. **Use appropriate types**: Choose the most specific type that represents your data (e.g., `Literal` for fixed values, `StrEnum` for string constants). + +2. **Error messages**: Custom converters should provide clear error messages for invalid input. + +3. **Performance**: Simple built-in converters are faster than complex custom ones. Use built-in types when possible. + +4. **Validation**: Type converters handle format conversion, but additional business logic validation should be done in your request handlers. + +/// From e606dbfb785463fb360922bd0b5d7471da9d1bd3 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Mon, 6 Oct 2025 06:48:06 +0200 Subject: [PATCH 12/14] Improve auth docs --- blacksheep/docs/authentication.md | 311 +----------------------------- blacksheep/docs/openapi.md | 302 +++++++++++++++++++++++++++++ 2 files changed, 309 insertions(+), 304 deletions(-) diff --git a/blacksheep/docs/authentication.md b/blacksheep/docs/authentication.md index 52f70e5..01c4c66 100644 --- a/blacksheep/docs/authentication.md +++ b/blacksheep/docs/authentication.md @@ -1266,313 +1266,16 @@ published in a dedicated library: [`guardpost`](https://github.com/neoteroi/guardpost) ([in pypi](https://pypi.org/project/guardpost/)). -## Documenting authentication - -Since version `2.4.2`, BlackSheep automatically generates OpenAPI Documentation for -authentication handlers when using the built-in authentication classes. This means -that when you configure API Key, Basic, or JWT Bearer the -corresponding security schemes are automatically added to your OpenAPI specification. - -### Automatic documentation for built-in handlers - -The following built-in authentication handlers are automatically documented: - -- **APIKeyAuthentication** → generates `ApiKey` security scheme. -- **BasicAuthentication** → generates `HTTP Basic` security scheme. -- **JWTBearerAuthentication** → generates `HTTP Bearer` security scheme with JWT format. - -```python -from blacksheep import Application -from blacksheep.server.authentication.apikey import APIKey, APIKeyAuthentication -from blacksheep.server.authentication.basic import BasicAuthentication, BasicCredentials -from blacksheep.server.authentication.jwt import JWTBearerAuthentication -from blacksheep.server.openapi.v3 import OpenAPIHandler -from openapidocs.v3 import Info -from essentials.secrets import Secret - -app = Application() - -# Configure OpenAPI documentation -docs = OpenAPIHandler(info=Info(title="My API", version="1.0.0")) -docs.bind_app(app) - -# These authentication handlers will be automatically documented -app.use_authentication().add( - APIKeyAuthentication( - APIKey(secret=Secret("$API_SECRET")), - param_name="X-API-Key", - description="API key for authentication" # ⟵ appears in OpenAPI docs - ) -).add( - BasicAuthentication( - BasicCredentials( - username="admin", - password=Secret("$ADMIN_PASSWORD") - ), - description="Basic authentication for admin users" # ⟵ appears in OpenAPI docs - ) -).add( - JWTBearerAuthentication( - authority="https://your-authority.com", - valid_audiences=["your-audience"] - # Automatically documented as Bearer JWT - ) -) -``` - -The generated OpenAPI specification will include: - -```yaml -components: - securitySchemes: - ApiKey: - type: apiKey - name: X-API-Key - in: header - description: API key for authentication - Basic: - type: http - scheme: basic - description: Basic authentication for admin users - Bearer: - type: http - scheme: bearer - bearerFormat: JWT -security: - - ApiKey: [] - - Basic: [] - - JWTBearerAuthentication: [] -``` - -### Custom SecuritySchemeHandler - -To control how your custom authentication handlers are documented, implement the -`SecuritySchemeHandler` abstract class: - -```python -from typing import Iterable, Tuple -from blacksheep import Application -from blacksheep.server.openapi.v3 import OpenAPIHandler, SecuritySchemeHandler -from blacksheep.server.authentication import AuthenticationHandler -from guardpost import AuthenticationHandler, Identity -from openapidocs.v3 import Info, SecurityScheme, HTTPSecurity, SecurityRequirement - -# Custom authentication handler -class CustomTokenAuthentication(AuthenticationHandler): - def __init__(self, scheme: str = "CustomToken"): - self.scheme = scheme - self.description = "Custom token authentication" - - async def authenticate(self, context) -> Identity | None: - # Your custom authentication logic here - token = context.get_first_header(b"X-Custom-Token") - if token: - # Validate token and return identity - return Identity({"sub": "user123"}, self.scheme) - return None - - -# Custom security scheme handler -class CustomTokenSecuritySchemeHandler(SecuritySchemeHandler): - def get_security_schemes( - self, handler: AuthenticationHandler - ) -> Iterable[Tuple[str, SecurityScheme, SecurityRequirement]]: - if isinstance(handler, CustomTokenAuthentication): - yield handler.scheme, HTTPSecurity( - scheme="bearer", - bearer_format="CustomToken", - description=handler.description - ), SecurityRequirement(handler.scheme, []) - - -app = Application() - -# Configure OpenAPI with custom security scheme handler -docs = OpenAPIHandler(info=Info(title="My API", version="1.0.0")) -docs.security_schemes_handlers.append(CustomTokenSecuritySchemeHandler()) -docs.bind_app(app) - -# Configure authentication -app.use_authentication().add(CustomTokenAuthentication()) -``` - -### Multiple authentication methods - -When you configure multiple authentication handlers, they are all documented and -the OpenAPI specification allows clients to choose any of the supported methods: - -```python -from blacksheep import Application, get -from blacksheep.server.authentication.apikey import APIKey, APIKeyAuthentication -from blacksheep.server.authentication.jwt import JWTBearerAuthentication -from blacksheep.server.authorization import auth -from blacksheep.server.openapi.v3 import OpenAPIHandler -from openapidocs.v3 import Info -from essentials.secrets import Secret - -app = Application() - -docs = OpenAPIHandler(info=Info(title="Multi-Auth API", version="1.0.0")) -docs.bind_app(app) - -# Configure multiple authentication methods -app.use_authentication().add( - # API Key authentication - APIKeyAuthentication( - APIKey(secret=Secret("$API_SECRET")), - param_name="X-API-Key", - description="API key authentication" - ) -).add( - # JWT Bearer authentication - JWTBearerAuthentication( - authority="https://your-authority.com", - valid_audiences=["your-audience"] - ) -) - -app.use_authorization() - - -@auth() # Accepts either API Key or JWT Bearer -@get("/protected") -async def protected_endpoint(): - return {"message": "Authenticated successfully"} -``` - -This generates OpenAPI documentation that shows both authentication methods are -supported, and clients can use either one. - -### Endpoint-specific authentication requirements - -You can document different authentication requirements for different endpoints: - -```python -from blacksheep import Application, get -from blacksheep.server.authentication.apikey import APIKey, APIKeyAuthentication -from blacksheep.server.authentication.basic import BasicAuthentication, BasicCredentials -from blacksheep.server.authentication.jwt import JWTBearerAuthentication -from blacksheep.server.authorization import allow_anonymous, auth -from blacksheep.server.openapi.common import SecurityInfo -from blacksheep.server.openapi.v3 import OpenAPIHandler, Info -from essentials.secrets import Secret - - -app = Application() - - -app.use_authentication().add( - APIKeyAuthentication( - APIKey( - secret=Secret("$API_SECRET"), # Obtained from API_SECRET env var - roles=["user"], - ), - param_name="X-API-Key", - ) -).add( - BasicAuthentication( - BasicCredentials( - username="admin", - password=Secret("$ADMIN_PASSWORD"), # Obtained from ADMIN_PASSWORD env var - roles=["admin"], - ) - ) -).add( - JWTBearerAuthentication( - valid_audiences=["myaudience"], - valid_issuers=["myapp"], - secret_key=Secret("$JWT_SECRET"), # Obtained from JWT_SECRET env var - ) -) - -app.use_authorization() - - -# See the generated docs and how they include security sections -docs = OpenAPIHandler(info=Info(title="Example API", version="0.0.1")) -docs.bind_app(app) - - -@auth(authentication_schemes=["ApiKey"]) # Only API Key authentication -@docs(security=[SecurityInfo("ApiKey", [])]) -@get("/api-key-only") -async def api_key_only(): - return {"message": "API Key required"} - - -@auth(authentication_schemes=["Bearer"]) # Only JWT Bearer authentication -@docs(security=[SecurityInfo("Bearer", [])]) -@get("/jwt-only") -async def jwt_only(): - return {"message": "JWT Bearer required"} - - -@allow_anonymous() -@get("/public") -async def public_endpoint(): - return {"message": "Public endpoint"} -``` - -### Legacy approach (before v2.4.2) - -Before version `2.4.2`, users needed to manually configure security schemes using -the `on_docs_created` event handler: - -```python -from blacksheep import Application -from blacksheep.server.openapi.v3 import OpenAPIHandler -from openapidocs.v3 import Info, APIKeySecurity, ParameterLocation, SecurityRequirement - -app = Application() - -docs = OpenAPIHandler(info=Info(title="My API", version="1.0.0")) - - -# Legacy approach - manual security scheme configuration -@docs.events.on_docs_created -def configure_security_schemes(openapi_docs): - # Manually add security schemes - if openapi_docs.components is None: - openapi_docs.components = Components() - if openapi_docs.components.security_schemes is None: - openapi_docs.components.security_schemes = {} - - # Add API Key security scheme - openapi_docs.components.security_schemes["ApiKey"] = APIKeySecurity( - name="X-API-Key", - in_=ParameterLocation.HEADER, - description="API key for authentication" - ) - - # Set global security requirement - openapi_docs.security = [SecurityRequirement("ApiKey", [])] - - -docs.bind_app(app) - -# Configure your custom authentication handler -# app.use_authentication().add(YourCustomHandler()) -``` - -/// admonition | Recommendation. - type: tip - -**Use built-in handlers when possible**: The built-in authentication handlers -(APIKeyAuthentication, BasicAuthentication, JWTBearerAuthentication) provide -automatic OpenAPI documentation and are well-tested. - -**For custom handlers**: Implement a `SecuritySchemeHandler` to ensure your -authentication methods are properly documented in the OpenAPI specification. - -**Migration from legacy approach**: If you're upgrading from a version before 2.4.2, -consider replacing manual security scheme configuration with built-in handlers or -custom SecuritySchemeHandler implementations. - -/// - ## Next While authentication focuses on *identifying* users, authorization determines whether a user *is permitted* to perform the requested action. The next page describes the built-in [authorization strategy](authorization.md) in BlackSheep. + +/// note | Documenting authentication. + +For information on how to document authentication schemes in OpenAPI +Specification files, refer to [_Documenting authentication_](./openapi.md#documenting-authentication). + +/// diff --git a/blacksheep/docs/openapi.md b/blacksheep/docs/openapi.md index 703f0c4..0680e23 100644 --- a/blacksheep/docs/openapi.md +++ b/blacksheep/docs/openapi.md @@ -11,6 +11,7 @@ OpenAPI Specification file. This page describes the following: - [X] Expose the documentation for anonymous access. - [X] Options to display OpenAPI Documentation. - [X] How to implement a custom `UIProvider`. +- [X] How to document authentication schemes. ## Introduction to OpenAPI Documentation @@ -989,6 +990,307 @@ class CustomOpenAPIHandler(OpenAPIHandler): return handler.__name__.capitalize().replace("_", " ") ``` +## Documenting authentication + +Since version `2.4.2`, BlackSheep automatically generates OpenAPI Documentation for +authentication handlers when using the built-in [authentication classes](./authentication.md). +This means that when you configure API Key, Basic, or JWT Bearer, the +corresponding security schemes are automatically added to your OpenAPI +specification. + +### Automatic documentation for built-in handlers + +The following built-in authentication handlers are automatically documented: + +- **APIKeyAuthentication** → generates `ApiKey` security scheme. +- **BasicAuthentication** → generates `HTTP Basic` security scheme. +- **JWTBearerAuthentication** → generates `HTTP Bearer` security scheme with JWT format. + +```python +from blacksheep import Application +from blacksheep.server.authentication.apikey import APIKey, APIKeyAuthentication +from blacksheep.server.authentication.basic import BasicAuthentication, BasicCredentials +from blacksheep.server.authentication.jwt import JWTBearerAuthentication +from blacksheep.server.openapi.v3 import OpenAPIHandler +from openapidocs.v3 import Info +from essentials.secrets import Secret + +app = Application() + +# Configure OpenAPI documentation +docs = OpenAPIHandler(info=Info(title="My API", version="1.0.0")) +docs.bind_app(app) + +# These authentication handlers will be automatically documented +app.use_authentication().add( + APIKeyAuthentication( + APIKey(secret=Secret("$API_SECRET")), + param_name="X-API-Key", + description="API key for authentication" # ⟵ appears in OpenAPI docs + ) +).add( + BasicAuthentication( + BasicCredentials( + username="admin", + password=Secret("$ADMIN_PASSWORD") + ), + description="Basic authentication for admin users" # ⟵ appears in OpenAPI docs + ) +).add( + JWTBearerAuthentication( + authority="https://your-authority.com", + valid_audiences=["your-audience"] + # Automatically documented as Bearer JWT + ) +) +``` + +The generated OpenAPI specification will include: + +```yaml +components: + securitySchemes: + ApiKey: + type: apiKey + name: X-API-Key + in: header + description: API key for authentication + Basic: + type: http + scheme: basic + description: Basic authentication for admin users + Bearer: + type: http + scheme: bearer + bearerFormat: JWT +security: + - ApiKey: [] + - Basic: [] + - JWTBearerAuthentication: [] +``` + +### Custom SecuritySchemeHandler + +To control how your custom authentication handlers are documented, implement the +`SecuritySchemeHandler` abstract class: + +```python +from typing import Iterable, Tuple +from blacksheep import Application +from blacksheep.server.openapi.v3 import OpenAPIHandler, SecuritySchemeHandler +from blacksheep.server.authentication import AuthenticationHandler +from guardpost import AuthenticationHandler, Identity +from openapidocs.v3 import Info, SecurityScheme, HTTPSecurity, SecurityRequirement + +# Custom authentication handler +class CustomTokenAuthentication(AuthenticationHandler): + def __init__(self, scheme: str = "CustomToken"): + self.scheme = scheme + self.description = "Custom token authentication" + + async def authenticate(self, context) -> Identity | None: + # Your custom authentication logic here + token = context.get_first_header(b"X-Custom-Token") + if token: + # Validate token and return identity + return Identity({"sub": "user123"}, self.scheme) + return None + + +# Custom security scheme handler +class CustomTokenSecuritySchemeHandler(SecuritySchemeHandler): + def get_security_schemes( + self, handler: AuthenticationHandler + ) -> Iterable[Tuple[str, SecurityScheme, SecurityRequirement]]: + if isinstance(handler, CustomTokenAuthentication): + yield handler.scheme, HTTPSecurity( + scheme="bearer", + bearer_format="CustomToken", + description=handler.description + ), SecurityRequirement(handler.scheme, []) + + +app = Application() + +# Configure OpenAPI with custom security scheme handler +docs = OpenAPIHandler(info=Info(title="My API", version="1.0.0")) +docs.security_schemes_handlers.append(CustomTokenSecuritySchemeHandler()) +docs.bind_app(app) + +# Configure authentication +app.use_authentication().add(CustomTokenAuthentication()) +``` + +### Multiple authentication methods + +When you configure multiple authentication handlers, they are all documented and +the OpenAPI specification allows clients to choose any of the supported methods: + +```python +from blacksheep import Application, get +from blacksheep.server.authentication.apikey import APIKey, APIKeyAuthentication +from blacksheep.server.authentication.jwt import JWTBearerAuthentication +from blacksheep.server.authorization import auth +from blacksheep.server.openapi.v3 import OpenAPIHandler +from openapidocs.v3 import Info +from essentials.secrets import Secret + +app = Application() + +docs = OpenAPIHandler(info=Info(title="Multi-Auth API", version="1.0.0")) +docs.bind_app(app) + +# Configure multiple authentication methods +app.use_authentication().add( + # API Key authentication + APIKeyAuthentication( + APIKey(secret=Secret("$API_SECRET")), + param_name="X-API-Key", + description="API key authentication" + ) +).add( + # JWT Bearer authentication + JWTBearerAuthentication( + authority="https://your-authority.com", + valid_audiences=["your-audience"] + ) +) + +app.use_authorization() + + +@auth() # Accepts either API Key or JWT Bearer +@get("/protected") +async def protected_endpoint(): + return {"message": "Authenticated successfully"} +``` + +This generates OpenAPI documentation that shows both authentication methods are +supported, and clients can use either one. + +### Endpoint-specific authentication requirements + +You can document different authentication requirements for different endpoints: + +```python +from blacksheep import Application, get +from blacksheep.server.authentication.apikey import APIKey, APIKeyAuthentication +from blacksheep.server.authentication.basic import BasicAuthentication, BasicCredentials +from blacksheep.server.authentication.jwt import JWTBearerAuthentication +from blacksheep.server.authorization import allow_anonymous, auth +from blacksheep.server.openapi.common import SecurityInfo +from blacksheep.server.openapi.v3 import OpenAPIHandler, Info +from essentials.secrets import Secret + + +app = Application() + + +app.use_authentication().add( + APIKeyAuthentication( + APIKey( + secret=Secret("$API_SECRET"), # Obtained from API_SECRET env var + roles=["user"], + ), + param_name="X-API-Key", + ) +).add( + BasicAuthentication( + BasicCredentials( + username="admin", + password=Secret("$ADMIN_PASSWORD"), # Obtained from ADMIN_PASSWORD env var + roles=["admin"], + ) + ) +).add( + JWTBearerAuthentication( + valid_audiences=["myaudience"], + valid_issuers=["myapp"], + secret_key=Secret("$JWT_SECRET"), # Obtained from JWT_SECRET env var + ) +) + +app.use_authorization() + + +# See the generated docs and how they include security sections +docs = OpenAPIHandler(info=Info(title="Example API", version="0.0.1")) +docs.bind_app(app) + + +@auth(authentication_schemes=["ApiKey"]) # Only API Key authentication +@docs(security=[SecurityInfo("ApiKey", [])]) +@get("/api-key-only") +async def api_key_only(): + return {"message": "API Key required"} + + +@auth(authentication_schemes=["Bearer"]) # Only JWT Bearer authentication +@docs(security=[SecurityInfo("Bearer", [])]) +@get("/jwt-only") +async def jwt_only(): + return {"message": "JWT Bearer required"} + + +@allow_anonymous() +@get("/public") +async def public_endpoint(): + return {"message": "Public endpoint"} +``` + +### For more granular control + +Before version `2.4.2`, or for full control on the generated OpenAPI +specification file, you can manually configure security schemes using +the `on_docs_created` event handler: + +```python +from blacksheep import Application +from blacksheep.server.openapi.v3 import OpenAPIHandler +from openapidocs.v3 import Info, APIKeySecurity, ParameterLocation, SecurityRequirement + +app = Application() + +docs = OpenAPIHandler(info=Info(title="My API", version="1.0.0")) + + +# Manual security scheme configuration +@docs.events.on_docs_created +def configure_security_schemes(openapi_docs): + # Manually add security schemes + if openapi_docs.components is None: + openapi_docs.components = Components() + if openapi_docs.components.security_schemes is None: + openapi_docs.components.security_schemes = {} + + # Add API Key security scheme + openapi_docs.components.security_schemes["ApiKey"] = APIKeySecurity( + name="X-API-Key", + in_=ParameterLocation.HEADER, + description="API key for authentication" + ) + + # Set global security requirement + openapi_docs.security = [SecurityRequirement("ApiKey", [])] + + +docs.bind_app(app) + +# Configure your custom authentication handler +# app.use_authentication().add(YourCustomHandler()) +``` + +/// admonition | Recommendation. + type: tip + +**Use built-in handlers when possible**: The built-in authentication handlers +(APIKeyAuthentication, BasicAuthentication, JWTBearerAuthentication) provide +automatic OpenAPI documentation and are well-tested. + +**For custom handlers**: Implement a dedicated `SecuritySchemeHandler` to +ensure your authentication methods are documented in the OpenAPI specification. + +/// ### For more details From edeb19faaf123c4dade4457d279df0fe84e79f23 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Mon, 6 Oct 2025 06:52:43 +0200 Subject: [PATCH 13/14] Update authorization.md --- blacksheep/docs/authorization.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/blacksheep/docs/authorization.md b/blacksheep/docs/authorization.md index a994568..e3ab13f 100644 --- a/blacksheep/docs/authorization.md +++ b/blacksheep/docs/authorization.md @@ -7,6 +7,7 @@ This page covers: - [X] How to use the built-in authorization strategy. - [X] How to apply authorization rules to request handlers. +- [X] How to require roles in request handlers. It is recommended to review the [authentication documentation](authentication.md) before proceeding with this page. @@ -259,6 +260,21 @@ async def only_for_user_authenticated_with_github(): return ok("example") ``` +## How to request roles for handlers. + + + +=== "Since version 2.4.2" + + Since version 2.4.2, the framework includes built-in features to request + _sufficient_ roles (any one is enough) to authorize web requests. + +=== "Before version 2.4.2" + + Before `2.4.2`, the framework did not include any specific code to define + roles for authorization, and required defining a _Policy_ that would + check for the desired property on the request context. + ## Failure response codes When a request fails because of authorization reasons, the web framework From aed9b1f959850d4aa0185b459afbce3a90054976 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Mon, 6 Oct 2025 19:54:52 +0200 Subject: [PATCH 14/14] Fix --- blacksheep/docs/authorization.md | 80 ++++++++++++++++++++++++++++---- blacksheep/docs/binders.md | 48 +++++++------------ 2 files changed, 88 insertions(+), 40 deletions(-) diff --git a/blacksheep/docs/authorization.md b/blacksheep/docs/authorization.md index e3ab13f..8a3e8e3 100644 --- a/blacksheep/docs/authorization.md +++ b/blacksheep/docs/authorization.md @@ -260,20 +260,82 @@ async def only_for_user_authenticated_with_github(): return ok("example") ``` -## How to request roles for handlers. +## Authorizing by roles - +/// tab | Since version 2.4.2 -=== "Since version 2.4.2" +Since version `2.4.2`, the framework includes built-in features to require +_sufficient_ roles (any one is enough) to authorize web requests. The authenticated +user object must have a `roles` property of type `List[str]`. + +```python +from blacksheep.server.authorization import Policy, auth - Since version 2.4.2, the framework includes built-in features to request - _sufficient_ roles (any one is enough) to authorize web requests. -=== "Before version 2.4.2" +app.use_authentication(...) # configure as desired + + +# requires a user with a roles property containing the string "admin" ↓ +@auth(roles=["admin"]) +async def only_for_admins(): + ... +``` - Before `2.4.2`, the framework did not include any specific code to define - roles for authorization, and required defining a _Policy_ that would - check for the desired property on the request context. +Examples: + +- When using JWT Bearer authentication, the JWT payload must have a `roles` claim with + the desired roles, for authorization to succeed. +- When using Basic authentication or API Key authentication with the built-in classes, + refer to the documentation at [_Authentication_](./authentication.md) for examples + on how to configure roles or claims on the identity object obtained after successful + authentication. +- When using custom authentication handlers, implement the desired logic and configure + `Identity` objects with the desired roles. + +```python +class MyAuthenticationHandler(AuthenticationHandler): + def authenticate(self, context: Request) -> Identity | None: + # TODO: implement your own authentication logic, handle roles as desired + return Identity({"sub": "***", "roles": []}, self.scheme) +``` + +/// + +/// tab | Before version 2.4.2 + +Before `2.4.2`, the framework did not include any specific code to define +roles for authorization, and required defining a _Policy_ that would +check for the desired property on the request context. + +```python +from guardpost import ( + AuthenticationHandler, + Identity, + User, + AuthorizationContext, + Requirement, +) +from guardpost.common import AuthenticatedRequirement + + +class AdminRequirement(Requirement): + def handle(self, context: AuthorizationContext): + identity = context.identity + + # Your own logic to check identity claims… + if identity is not None and identity.claims.get("role") == "admin": + context.succeed(self) + +app.use_authentication(...) # configure as desired + +app.use_authorization().add(Policy("admin", AdminRequirement())) + +@auth("admin") +async def only_for_admins(): + ... +``` + +/// ## Failure response codes diff --git a/blacksheep/docs/binders.md b/blacksheep/docs/binders.md index c00a9ce..957699d 100644 --- a/blacksheep/docs/binders.md +++ b/blacksheep/docs/binders.md @@ -279,7 +279,10 @@ def home(something: FromCustomValue): ## Custom Convert Functions in BoundValue Classes -Since version `2.4.1`, custom `BoundValue` classes can define a `convert` class method to transform Python objects from parsed JSON into more specific types. This is particularly useful when you need to apply custom validation or transformation logic during the binding process. +Since version `2.4.1`, custom `BoundValue` classes can define a `convert` class method +to transform Python objects from parsed JSON into more specific types. This is +particularly useful when you need to apply custom validation or transformation logic +during the binding process. ### Defining a Convert Function @@ -299,7 +302,8 @@ class CustomData(BoundValue[Dict[str, Any]]): def convert(cls, value: Any) -> Dict[str, Any]: """ Convert the parsed JSON value into the desired format. - This method is called after JSON parsing but before creating the BoundValue instance. + This method is called after JSON parsing but before creating the BoundValue + instance. """ if isinstance(value, dict): # Apply custom validation and transformation @@ -409,21 +413,25 @@ class ValidatedInput(BoundValue[dict]): return value ``` -When the `convert` method raises an exception, BlackSheep automatically returns a `400 Bad Request` response with the error message. +When the `convert` method raises an exception, BlackSheep automatically returns a `400 +Bad Request` response with the error message. /// admonition | Convert Method Behavior type: info -- The `convert` method is called **after** JSON parsing but **before** the `BoundValue` instance is created. - It receives the parsed Python object (dict, list, etc.) as input. - The return value becomes the `value` property of the `BoundValue` instance. -- Exceptions raised in `convert` methods are automatically converted to `400 Bad Request` responses. +- Exceptions raised in `convert` methods are automatically converted to `400 Bad + Request` responses. /// ## Type Converters -Since version `2.4.1`, BlackSheep provides a flexible type conversion system through the `TypeConverter` abstract class. This system allows automatic conversion of string representations from request parameters (query, headers, route, etc.) into specific Python types. +Since version `2.4.1`, BlackSheep provides a flexible type conversion system through the +`TypeConverter` abstract class. This system allows automatic conversion of string +representations from request parameters (query, headers, route, etc.) into specific +Python types. ### Built-in Type Converters @@ -611,7 +619,9 @@ async def get_users(status: Status): ### Type Converter Priority -Converters are evaluated in the order they appear in the `converters` list. Built-in converters are registered by default, and custom converters are typically appended to the list. +Converters are evaluated in the order they appear in the `converters` list. Built-in +converters are registered by default, and custom converters are typically appended to +the list. ```python from blacksheep.server.bindings.converters import converters @@ -623,27 +633,3 @@ for converter in converters: # Add custom converter with priority (insert at beginning) converters.insert(0, YourCustomConverter()) ``` - -/// admonition | Version Requirements - type: info - -- **StrEnum and IntEnum support**: Requires Python 3.11 or later -- **Literal support**: Available in all supported Python versions (3.8+) -- **Custom TypeConverter**: Available since BlackSheep 2.4.1 - -For older Python versions, you can achieve similar functionality using regular `Enum` classes or custom validation in your request handlers. - -/// - -/// admonition | Best Practices - type: tip - -1. **Use appropriate types**: Choose the most specific type that represents your data (e.g., `Literal` for fixed values, `StrEnum` for string constants). - -2. **Error messages**: Custom converters should provide clear error messages for invalid input. - -3. **Performance**: Simple built-in converters are faster than complex custom ones. Use built-in types when possible. - -4. **Validation**: Type converters handle format conversion, but additional business logic validation should be done in your request handlers. - -///