Skip to content

Commit b806f96

Browse files
authored
Merge pull request #2 from feteu/develop
v1.0.1
2 parents 8dd7591 + 5c535f6 commit b806f96

File tree

17 files changed

+446
-70
lines changed

17 files changed

+446
-70
lines changed

CONTRIBUTING.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Contributing to ASGI Claim Validator
22

3-
Thanks for thinking about contributing to ASGI Request Duration! I welcome your help and can't wait to see what you'll bring to the table.
3+
Thanks for thinking about contributing to ASGI Claim Validator! I welcome your help and can't wait to see what you'll bring to the table.
44

55
## How to Contribute
66

README.md

+169-46
Original file line numberDiff line numberDiff line change
@@ -8,58 +8,98 @@
88
[![Build Status build/pypi](https://img.shields.io/github/actions/workflow/status/feteu/asgi-claim-validator/publish-pypi.yaml?label=publish-pypi)](https://github.com/feteu/asgi-claim-validator/actions/workflows/publish-pypi.yaml)
99
[![Build Status test](https://img.shields.io/github/actions/workflow/status/feteu/asgi-claim-validator/test.yaml?label=test)](https://github.com/feteu/asgi-claim-validator/actions/workflows/test.yaml)
1010

11-
# asgi-claim-validator
11+
# asgi-claim-validator 🚀
1212

1313
A focused ASGI middleware for validating additional claims within JWT tokens to enhance token-based workflows.
1414

15-
## Overview
15+
> **Note:** If you find this project useful, please consider giving it a star ⭐ on GitHub. This helps prioritize its maintenance and development. If you encounter any typos, bugs 🐛, or have new feature requests, feel free to open an issue. I will be happy to address them.
16+
17+
18+
## Table of Contents 📑
19+
20+
1. [Overview 📖](#overview-)
21+
1. [Purpose 🎯](#purpose-)
22+
2. [Key Features ✨](#key-features-)
23+
3. [Use Cases 💡](#use-cases-)
24+
4. [Compatibility 🤝](#compatibility-)
25+
2. [Installation 🛠️](#installation-)
26+
3. [Usage 📚](#usage-)
27+
1. [Basic Usage 🌟](#basic-usage)
28+
2. [Configuration ⚙️](#configuration-)
29+
3. [Error Handlers 🚨](#error-handlers-)
30+
4. [Examples 📝](#examples-)
31+
5. [Testing 🧪](#testing-)
32+
6. [Contributing 🤝](#contributing-)
33+
7. [License 📜](#license-)
34+
35+
36+
## Overview 📖
1637

1738
`asgi-claim-validator` is an ASGI middleware designed to validate additional claims within JWT tokens. Built in addition to the default JWT verification implementation of Connexion, it enhances token-based workflows by ensuring that specific claims are present and meet certain criteria before allowing access to protected endpoints. This middleware allows consumers to validate claims on an endpoint/method level and is compatible with popular ASGI frameworks such as Starlette, FastAPI, and Connexion.
1839

19-
## Features
40+
### Purpose 🎯
41+
42+
The primary purpose of `asgi-claim-validator` is to provide an additional layer of security by validating specific claims within JWT tokens. This ensures that only requests with valid and authorized tokens can access protected resources. The middleware is highly configurable, allowing developers to define essential claims, allowed values, and whether blank values are permitted. It also supports path and method filtering, enabling claim validation to be applied selectively based on the request path and HTTP method.
43+
44+
### Key Features ✨
2045

2146
- **Claim Validation**: Validate specific claims within JWT tokens, such as `sub`, `iss`, `aud`, `exp`, `iat`, and `nbf`.
2247
- **Customizable Claims**: Define essential claims, allowed values, and whether blank values are permitted.
2348
- **Path and Method Filtering**: Apply claim validation to specific paths and HTTP methods.
2449
- **Exception Handling**: Integrate with custom exception handlers to provide meaningful error responses.
2550
- **Logging**: Log validation errors for debugging and monitoring purposes.
51+
- **Flexible Configuration**: Easily configure the middleware using a variety of options to suit different use cases.
52+
- **Middleware Positioning**: Integrate the middleware at different positions within the ASGI application stack.
53+
- **Token Extraction**: Extract tokens from various parts of the request, such as headers, cookies, or query parameters.
54+
- **Custom Claim Validators**: Implement custom claim validation logic by providing your own validation functions.
55+
- **Support for Multiple Frameworks**: Compatible with popular ASGI frameworks such as Starlette, FastAPI, and Connexion.
56+
- **Performance Optimization**: Efficiently handle claim validation with minimal impact on request processing time.
57+
- **Extensive Test Coverage**: Comprehensive test suite to ensure reliability and correctness of the middleware.
58+
59+
### Use Cases 💡
2660

27-
## Installation
61+
- **API Security**: Enhance the security of your API by ensuring that only requests with valid JWT tokens and specific claims can access protected endpoints.
62+
- **Role-Based Access Control**: Implement role-based access control by validating claims that represent user roles and permissions.
63+
- **Compliance**: Ensure compliance with security policies by enforcing the presence and validity of specific claims within JWT tokens.
64+
- **Custom Authentication Logic**: Implement custom authentication logic by providing your own claim validation functions.
2865

29-
Install the package using pip:
66+
### Compatibility 🤝
67+
68+
`asgi-claim-validator` is compatible with popular ASGI frameworks such as Starlette, FastAPI, and Connexion. It can be easily integrated into existing ASGI applications and configured to suit various use cases and requirements.
69+
70+
By using `asgi-claim-validator`, you can enhance the security and flexibility of your token-based authentication workflows, ensuring that only authorized requests can access your protected resources.
71+
72+
73+
## Installation 🛠️
74+
75+
To install the `asgi-claim-validator` package, use the following pip command:
3076

3177
```sh
3278
pip install asgi-claim-validator
3379
```
3480

35-
## Usage
3681

37-
### Basic Usage
82+
## Usage 📚
3883

39-
Here's an example of how to use `asgi-claim-validator` with Starlette:
84+
### Basic Usage 🌟
4085

41-
```python
42-
from starlette.applications import Starlette
43-
from starlette.requests import Request
44-
from starlette.responses import JSONResponse
45-
from starlette.routing import Route
46-
from asgi_claim_validator import ClaimValidatorMiddleware
86+
Below is an example of how to integrate `ClaimValidatorMiddleware` with a Connexion application. This middleware validates specific claims within JWT tokens for certain endpoints.
4787

48-
async def secured_endpoint(request: Request) -> JSONResponse:
49-
return JSONResponse({"message": "secured"})
88+
The `ClaimValidatorMiddleware` requires several parameters to function correctly. The `claims_callable` parameter is a callable that extracts token information from the Connexion context. This parameter must be specified and is typically dependent on the framework being used. The `secured` parameter is a dictionary that defines the secured paths and the claims that need to be validated. For instance, in the provided example, the `/secured` path requires the `sub` claim to be `admin` and the `iss` claim to be `https://example.com` for GET requests. The `skipped` parameter is a dictionary that specifies the paths and methods that should be excluded from validation. In the example, the `/skipped` path is skipped for GET requests.
89+
90+
```python
91+
from asgi_claim_validator.middleware import ClaimValidatorMiddleware
92+
from connexion import AsyncApp
5093

51-
app = Starlette(routes=[
52-
Route("/secured", secured_endpoint, methods=["GET"]),
53-
])
94+
# Create a Connexion application
95+
app = AsyncApp(__name__, specification_dir="spec")
5496

97+
# Add the ClaimValidatorMiddleware
5598
app.add_middleware(
5699
ClaimValidatorMiddleware,
57-
claims_callable=lambda: {
58-
"sub": "admin",
59-
"iss": "https://example.com",
60-
},
100+
claims_callable=lambda scope: scope["extensions"]["connexion_context"]["token_info"],
61101
secured={
62-
"^/secured$": {
102+
"^/secured/?$": {
63103
"GET": {
64104
"sub": {
65105
"essential": True,
@@ -72,52 +112,135 @@ app.add_middleware(
72112
"values": ["https://example.com"],
73113
},
74114
},
75-
}
115+
},
116+
},
117+
skipped={
118+
"^/skipped/?$": ["GET"],
76119
},
77120
)
78121
```
79122

80-
## Advanced Usage
81123

82-
### Custom Exception Handlers
124+
### Configuration ⚙️
125+
126+
The `ClaimValidatorMiddleware` requires two main configuration pieces: `secured` and `skipped`. These configurations are validated using JSON schemas to ensure correctness.
127+
128+
> **Note:** The path regex patterns provided in the `secured` and `skipped` parameters will be automatically escaped by the middleware.
129+
130+
#### Secured Configuration
131+
132+
The `secured` configuration is a dictionary that defines the paths and the claims that need to be validated. Each path is associated with a dictionary of HTTP methods, and each method is associated with a dictionary of claims. Each claim can have the following properties:
133+
- `essential`: A boolean indicating whether the claim is essential.
134+
- `allow_blank`: A boolean indicating whether blank values are allowed.
135+
- `values`: A list of allowed values for the claim.
136+
137+
Example:
138+
```python
139+
secured={
140+
"^/secured/?$": {
141+
"GET": {
142+
"sub": {
143+
"essential": True,
144+
"allow_blank": False,
145+
"values": ["admin"],
146+
},
147+
"iss": {
148+
"essential": True,
149+
"allow_blank": False,
150+
"values": ["https://example.com"],
151+
},
152+
},
153+
},
154+
}
155+
```
156+
157+
#### Skipped Configuration
158+
159+
The `skipped` configuration is a dictionary that defines the paths and methods that should be excluded from validation. Each path is associated with a list of HTTP methods.
83160

84-
Integrate `asgi-claim-validator` with custom exception handlers to provide meaningful error responses. Below are examples for Starlette and Connexion. Refer to the specific framework examples in the [examples](examples) directory for detailed implementation.
161+
Example:
162+
```python
163+
skipped={
164+
"^/skipped/?$": ["GET"],
165+
}
166+
```
85167

86-
### Middleware Configuration
168+
#### JSON Schema Validation
87169

88-
Configure the middleware with the following options:
170+
Both `secured` and `skipped` configurations are validated using JSON schemas to ensure their correctness. This validation helps catch configuration errors early and ensures that the middleware behaves as expected.
89171

90-
- **claims_callable**: A callable that returns the JWT claims to be validated.
91-
- **secured**: A dictionary defining the paths and methods that require claim validation.
92-
- **skipped**: A dictionary defining the paths and methods to be excluded from claim validation.
93-
- **raise_on_unspecified_path**: Raise an exception if the path is not specified in the `secured` or `skipped` dictionaries.
94-
- **raise_on_unspecified_method**: Raise an exception if the method is not specified for a secured path.
95172

96-
### Claim Validation Options
173+
### Error Handlers 🚨
97174

98-
Configure claims with the following options:
175+
To handle exceptions raised by this middleware, you can configure your framework (such as Starlette or Connexion) to catch and process them dynamically. For security reasons, the exception messages are kept generic, but you can customize them using the exception parameters.
99176

100-
- **essential**: Indicates if the claim is essential (default: `False`).
101-
- **allow_blank**: Indicates if blank values are allowed (default: `True`).
102-
- **values**: A list of allowed values for the claim.
177+
#### Connexion
178+
179+
```python
180+
from asgi_claim_validator import ClaimValidatorMiddleware, ClaimValidatorException
181+
from connexion import AsyncApp
182+
from connexion.lifecycle import ConnexionRequest, ConnexionResponse
103183

104-
## Examples
184+
# [...]
185+
186+
def claim_validator_error_handler(request: ConnexionRequest, exc: ClaimValidatorException) -> ConnexionResponse:
187+
return problem(detail=exc.detail, status=exc.status, title=exc.title)
188+
189+
app = AsyncApp(__name__, specification_dir="spec")
190+
app.add_error_handler(ClaimValidatorException, claim_validator_error_handler)
191+
192+
# [...]
193+
```
194+
195+
#### Starlette
196+
197+
```python
198+
from asgi_claim_validator import ClaimValidatorMiddleware, ClaimValidatorException
199+
from starlette.applications import Starlette
200+
from starlette.requests import Request
201+
from starlette.responses import JSONResponse
202+
203+
# [...]
204+
205+
async def claim_validator_error_handler(request: Request, exc: ClaimValidatorException) -> JSONResponse:
206+
return JSONResponse({"error": f"{exc.title}"}, status_code=exc.status)
207+
208+
exception_handlers = {
209+
ClaimValidatorException: claim_validator_error_handler
210+
}
211+
212+
app = Starlette(routes=routes, exception_handlers=exception_handlers)
213+
214+
# [...]
215+
```
216+
217+
## Examples 📝
105218

106219
### Starlette Example
107-
Refer to the [app.py](examples/starlette/simple/app.py) file for a complete example using Starlette.
220+
To see a complete example using Starlette, refer to the [app.py](examples/starlette/simple/app.py) file.
108221

109222
### Connexion Example
110-
Refer to the [app.py](examples/connexion/simple/app.py) file for a complete example using Connexion.
223+
Check out the [app.py](examples/connexion/simple/app.py) file for a simple example using Connexion. For a comprehensive example that demonstrates automatic extraction and validation of token claims with Connexion, see the [app.py](examples/connexion/complex/app.py) file.
111224

112-
## Testing
225+
## Testing 🧪
113226
Run the tests using `pytest`:
114227

115228
```sh
116229
poetry run pytest
117230
```
118231

119-
## Contributing
232+
### Scope:
233+
234+
- **Middleware Functionality**: Ensures correct validation of JWT claims and proper handling of secured and skipped paths.
235+
- **Exception Handling**: Verifies that custom exceptions are raised and handled appropriately.
236+
- **Configuration Validation**: Checks the correctness of middleware configuration for secured and skipped paths.
237+
- **Integration with Frameworks**: Confirms seamless integration with ASGI frameworks like Starlette and Connexion.
238+
- **Custom Claim Validators**: Tests the implementation and usage of custom claim validation logic.
239+
240+
241+
## Contributing 🤝
120242
Contributions are welcome! Please refer to the [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines on how to contribute to this project.
121243

122-
## License
244+
245+
## License 📜
123246
This project is licensed under the GNU GPLv3 License. See the [LICENSE](LICENSE) file for more details.

asgi_claim_validator/__init__.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,35 @@
1-
from asgi_claim_validator.decorators import validate_claims_callable
21
from asgi_claim_validator.exceptions import (
32
ClaimValidatorException,
3+
InvalidClaimsConfigurationException,
44
InvalidClaimsTypeException,
55
InvalidClaimValueException,
6+
InvalidSecuredConfigurationException,
7+
InvalidSkippedConfigurationException,
68
MissingEssentialClaimException,
79
UnauthenticatedRequestException,
810
UnspecifiedMethodAuthenticationException,
911
UnspecifiedPathAuthenticationException,
1012
)
1113
from asgi_claim_validator.middleware import ClaimValidatorMiddleware
12-
from asgi_claim_validator.types import SecuredCompiledType, SecuredType, SkippedCompiledType, SkippedType
14+
from asgi_claim_validator.types import (
15+
ClaimsCallableType,
16+
ClaimsType,
17+
SecuredCompiledType,
18+
SecuredType,
19+
SkippedCompiledType,
20+
SkippedType,
21+
)
1322

1423
__all__ = (
24+
"ClaimsCallableType",
25+
"ClaimsType",
1526
"ClaimValidatorException",
1627
"ClaimValidatorMiddleware",
28+
"InvalidClaimsConfigurationException",
1729
"InvalidClaimsTypeException",
1830
"InvalidClaimValueException",
31+
"InvalidSecuredConfigurationException",
32+
"InvalidSkippedConfigurationException",
1933
"MissingEssentialClaimException",
2034
"SecuredCompiledType",
2135
"SecuredType",
@@ -24,5 +38,4 @@
2438
"UnauthenticatedRequestException",
2539
"UnspecifiedMethodAuthenticationException",
2640
"UnspecifiedPathAuthenticationException",
27-
"validate_claims_callable",
2841
)

asgi_claim_validator/constants.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"TRACE",
1515
]
1616
_DEFAULT_ALL_HTTP_METHODS_REGEX_GROUP: str = f"({'|'.join(map(escape, (*_DEFAULT_ALL_HTTP_METHODS, *_DEFAULT_ANY_HTTP_METHODS)))})"
17-
_DEFAULT_CLAIMS_CALLABLE: ClaimsCallableType = lambda: dict()
17+
_DEFAULT_CLAIMS_CALLABLE: ClaimsCallableType = lambda scope: scope.get("", dict())
1818
_DEFAULT_RAISE_ON_INVALID_CLAIM: bool = True
1919
_DEFAULT_RAISE_ON_INVALID_CLAIMS_TYPE: bool = True
2020
_DEFAULT_RAISE_ON_MISSING_CLAIM: bool = True

asgi_claim_validator/decorators.py

+21-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@
1616
log = getLogger(__name__)
1717

1818
def validate_claims_callable() -> Callable:
19-
def decorator(func) -> Callable:
19+
"""
20+
Decorator to validate the claims_callable attribute of a class.
21+
22+
Raises:
23+
InvalidClaimsConfigurationException: If claims_callable is not a callable.
24+
"""
25+
def decorator(func: Callable) -> Callable:
2026
def wrapper(self, *args, **kwargs) -> Callable:
2127
claims = getattr(self, 'claims_callable', _DEFAULT_CLAIMS_CALLABLE)
2228
if not isinstance(claims, Callable):
@@ -26,7 +32,13 @@ def wrapper(self, *args, **kwargs) -> Callable:
2632
return decorator
2733

2834
def validate_secured() -> Callable:
29-
def decorator(func) -> Callable:
35+
"""
36+
Decorator to validate the secured attribute of a class against a JSON schema.
37+
38+
Raises:
39+
InvalidSecuredConfigurationException: If the secured attribute does not conform to the schema.
40+
"""
41+
def decorator(func: Callable) -> Callable:
3042
def wrapper(self, *args, **kwargs) -> Callable:
3143
secured = getattr(self, 'secured', None)
3244
try:
@@ -39,7 +51,13 @@ def wrapper(self, *args, **kwargs) -> Callable:
3951
return decorator
4052

4153
def validate_skipped() -> Callable:
42-
def decorator(func) -> Callable:
54+
"""
55+
Decorator to validate the skipped attribute of a class against a JSON schema.
56+
57+
Raises:
58+
InvalidSkippedConfigurationException: If the skipped attribute does not conform to the schema.
59+
"""
60+
def decorator(func: Callable) -> Callable:
4361
def wrapper(self, *args, **kwargs) -> Callable:
4462
skipped = getattr(self, 'skipped', None)
4563
try:

0 commit comments

Comments
 (0)