Skip to content

Commit 95ea0c3

Browse files
authored
Merge pull request #20 from OSSMafia/mnick/19
chore: expanded testing and documentation
2 parents 02b61f4 + 0fb631f commit 95ea0c3

File tree

9 files changed

+452
-36
lines changed

9 files changed

+452
-36
lines changed

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 0.0.8
2+
current_version = 0.0.9
33
commit = True
44
tag = True
55

README.md

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,27 +37,65 @@ clerk_config = ClerkConfig(jwks_url="https://your-clerk-frontend-api.clerk.accou
3737
clerk_auth_guard = ClerkHTTPBearer(config=clerk_config)
3838

3939
@app.get("/")
40-
async def read_root(credentials: HTTPAuthorizationCredentials | None = Depends(clerk_auth_guard)):
40+
async def read_root(credentials: HTTPAuthorizationCredentials = Depends(clerk_auth_guard)):
4141
return JSONResponse(content=jsonable_encoder(credentials))
4242
```
4343

44-
The returned `credentials` model will be either `None` or an `HTTPAuthorizationCredentials` object with these properties:
44+
The returned `credentials` model will be an `HTTPAuthorizationCredentials` object with these properties:
4545

4646
- `scheme`: Indicates the scheme of the Authorization header (Bearer)
4747
- `credentials`: Raw token received from the Authorization header
4848
- `decoded`: The payload of the decoded token
4949

5050
## Configuration Options
5151

52+
### Debug Mode
53+
54+
By default, the middleware suppresses exceptions in order to prevent logging sensitive information. You can change this behavior in the `ClerkHTTPBearer`:
55+
56+
```python
57+
clerk_auth_guard = ClerkHTTPBearer(config=clerk_config, debug_mode=True) # Set debug_mode=True
58+
```
59+
60+
61+
### Fixing Issued At Time (IAT) Errors
62+
63+
In some instances the system clock of an API may be slightly out-of-sync which can cause issues with verifying the `iat` claim.
64+
65+
This can be solved one of two ways:
66+
67+
1: Disable verifying the `iat` claim, there could be security implications by disabling so make sure it is secure for your use case.
68+
69+
```python
70+
clerk_config = ClerkConfig(
71+
jwks_url="https://your-clerk-frontend-api.clerk.accounts.dev/.well-known/jwks.json",
72+
verify_iat=False,
73+
)
74+
75+
clerk_auth_guard = ClerkHTTPBearer(config=clerk_config)
76+
```
77+
78+
2: Adding `leeway` which is a number of seconds to allow tolerance for drift, it can compensate for out-of-sync clocks.
79+
80+
```python
81+
clerk_config = ClerkConfig(
82+
jwks_url="https://your-clerk-frontend-api.clerk.accounts.dev/.well-known/jwks.json",
83+
leeway=5.0,
84+
)
85+
86+
clerk_auth_guard = ClerkHTTPBearer(config=clerk_config)
87+
```
88+
5289
### Disabling Auto Errors
5390

5491
By default, the middleware automatically returns 403 errors if the token is missing or invalid. You can disable this behavior:
5592

5693
```python
5794
clerk_config = ClerkConfig(
58-
jwks_url="https://your-clerk-frontend-api.clerk.accounts.dev/.well-known/jwks.json",
59-
auto_error=False
60-
)
95+
jwks_url="https://your-clerk-frontend-api.clerk.accounts.dev/.well-known/jwks.json"
96+
)
97+
98+
clerk_auth_guard = ClerkHTTPBearer(config=clerk_config, auto_error=False) # Set auto_error=False
6199
```
62100

63101
This allows requests to reach the endpoint for additional logic or custom error handling:
@@ -99,6 +137,15 @@ async def read_todo_list(request: Request):
99137
return {"message": f"Todo items for user {user_id}"}
100138
```
101139

140+
The class stored in `request.state.clerk_auth` looks like this:
141+
```python
142+
HTTPAuthorizationCredentials(
143+
decoded: Optional[dict], # Decoded JWT Token
144+
scheme: str = "Bearer",
145+
credentials: str, # Raw JWT Token
146+
)
147+
```
148+
102149
## Advanced Usage
103150

104151
### Role-Based Access Control

docker-compose.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ services:
2626
context: .
2727
dockerfile: dockerfile
2828
target: test
29+
environment:
30+
- JWT_SUB_CLAIM=1234567890
31+
- JWT_AUDIENCE_CLAIM=test
32+
- JWT_ISSUER_CLAIM=test_issuer
2933
networks:
3034
- default
3135
depends_on:

fastapi_clerk_auth/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from fastapi import HTTPException, Request
44
from fastapi.encoders import jsonable_encoder
5-
from fastapi.openapi.models import HTTPBearer as HTTPBearerModel
65
from fastapi.security import HTTPAuthorizationCredentials as FastAPIHTTPAuthorizationCredentials, HTTPBearer
76
from fastapi.security.utils import get_authorization_scheme_param
87
import jwt
@@ -19,6 +18,7 @@ class ClerkConfig(BaseModel):
1918
verify_exp: bool = True
2019
verify_aud: bool = False
2120
verify_iss: bool = False
21+
verify_iat: bool = True
2222
jwks_cache_keys: bool = False
2323
jwks_max_cached_keys: int = 16
2424
jwks_cache_set: bool = True
@@ -97,8 +97,6 @@ def __init__(
9797
description=description,
9898
auto_error=auto_error,
9999
)
100-
self.model = HTTPBearerModel(bearerFormat=bearerFormat, description=description)
101-
self.scheme_name = scheme_name or self.__class__.__name__
102100
self.auto_error = auto_error
103101
self.add_state = add_state
104102
self.config = config
@@ -137,6 +135,7 @@ def _decode_token(self, token: str) -> dict | None:
137135
"verify_exp": self.config.verify_exp,
138136
"verify_aud": self.config.verify_aud,
139137
"verify_iss": self.config.verify_iss,
138+
"verify_iat": self.config.verify_iat,
140139
},
141140
leeway=self.config.leeway,
142141
)
@@ -159,6 +158,7 @@ async def __call__(self, request: Request) -> Optional[HTTPAuthorizationCredenti
159158
return None
160159

161160
decoded_token: dict | None = self._decode_token(token=credentials)
161+
162162
if not decoded_token and self.auto_error:
163163
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Forbidden")
164164
response = HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials, decoded=decoded_token)

pyproject.toml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
[project]
22
name = "fastapi_clerk_auth"
3-
version = "0.0.8"
3+
version = "0.0.9"
44
description = "FastAPI Auth Middleware for Clerk (https://clerk.com)"
55
readme = "README.md"
66
requires-python = ">=3.9"
77
authors = [
88
{ name = "Matt Nick", email = "[email protected]" },
99
]
10-
maintainers = [
11-
{ name = "Matt Nick", email = "[email protected]" },
12-
]
1310
classifiers = [
1411
"Development Status :: 5 - Production/Stable",
1512
"Framework :: FastAPI",

tests/conftest.py

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,70 @@
44
import pytest
55

66

7+
def generate_jwt(payload: dict) -> str:
8+
with open("./mock_files/test_private_key.pem") as key_file:
9+
private_key = key_file.read()
10+
11+
return jwt.encode(payload, private_key, algorithm="RS256", headers={"kid": "ins_test_key_1"})
12+
13+
714
@pytest.fixture
8-
def jwt_token():
9-
return generate_jwt()
15+
def jwt_token_default():
16+
payload = {
17+
"sub": "1234567890",
18+
"iat": int(time()),
19+
"exp": int(time()) + 300,
20+
}
21+
return generate_jwt(payload)
1022

1123

12-
def generate_jwt():
13-
with open("./mock_files/test_private_key.pem") as key_file:
14-
private_key = key_file.read()
24+
@pytest.fixture
25+
def jwt_token_audience():
26+
payload = {
27+
"sub": "1234567890",
28+
"aud": "test",
29+
"iat": int(time()),
30+
"exp": int(time()) + 300,
31+
}
32+
return generate_jwt(payload)
1533

16-
payload = {"sub": "1234567890", "iat": int(time()), "exp": int(time()) + 300}
1734

18-
return jwt.encode(payload, private_key, algorithm="RS256", headers={"kid": "ins_test_key_1"})
35+
@pytest.fixture
36+
def jwt_token_issuer():
37+
payload = {
38+
"sub": "1234567890",
39+
"iat": int(time()),
40+
"exp": int(time()) + 300,
41+
"iss": "test_issuer",
42+
}
43+
return generate_jwt(payload)
44+
45+
46+
@pytest.fixture
47+
def jwt_token_iat_future():
48+
payload = {
49+
"sub": "1234567890",
50+
"iat": int(time()) + 600,
51+
"exp": int(time()) + 900,
52+
}
53+
return generate_jwt(payload)
54+
55+
56+
@pytest.fixture
57+
def jwt_token_iat_future_short_leeway():
58+
payload = {
59+
"sub": "1234567890",
60+
"iat": int(time()) + 10,
61+
"exp": int(time()) + 600,
62+
}
63+
return generate_jwt(payload)
64+
65+
66+
@pytest.fixture
67+
def jwt_token_expired():
68+
payload = {
69+
"sub": "1234567890",
70+
"iat": int(time()),
71+
"exp": int(time()) - 300,
72+
}
73+
return generate_jwt(payload)

0 commit comments

Comments
 (0)