Skip to content

Commit 47bc4be

Browse files
authored
Merge pull request #8 from OSSMafia/chore/bump-packages
chore: package updates and cleanup
2 parents fed574a + 139de9a commit 47bc4be

12 files changed

Lines changed: 221 additions & 73 deletions

File tree

.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.5
2+
current_version = 0.0.6
33
commit = True
44
tag = True
55

Makefile

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,35 @@
11
format:
2-
bash ./scripts/formatter.sh
3-
4-
lint:
5-
bash ./scripts/linter.sh
2+
docker build --target=format -t package:format -f dockerfile . && \
3+
docker run --rm \
4+
-v ./fastapi_clerk_auth:/app/fastapi_clerk_auth \
5+
-v ./.bumpversion.cfg:/app/.bumpversion.cfg \
6+
-v ./pyproject.toml:/app/pyproject.toml \
7+
package:format
8+
docker rmi package:format || true
69

710
version_patch:
8-
bash ./scripts/version_patch.sh
11+
docker build --target=bump_patch -t package:bumpversion -f dockerfile . && \
12+
docker run --rm \
13+
-v ./fastapi_clerk_auth:/app/fastapi_clerk_auth \
14+
-v ./.bumpversion.cfg:/app/.bumpversion.cfg \
15+
-v ./pyproject.toml:/app/pyproject.toml \
16+
package:bumpversion
17+
docker rmi package:bumpversion || true
918

1019
version_minor:
11-
bash ./scripts/version_minor.sh
20+
docker build --target=bump_minor -t package:bumpversion -f dockerfile . && \
21+
docker run --rm \
22+
-v ./fastapi_clerk_auth:/app/fastapi_clerk_auth \
23+
-v ./.bumpversion.cfg:/app/.bumpversion.cfg \
24+
-v ./pyproject.toml:/app/pyproject.toml \
25+
package:bumpversion
26+
docker rmi package:bumpversion || true
1227

1328
version_major:
14-
bash ./scripts/version_major.sh
29+
docker build --target=bump_major -t package:bumpversion -f dockerfile . && \
30+
docker run --rm \
31+
-v ./fastapi_clerk_auth:/app/fastapi_clerk_auth \
32+
-v ./.bumpversion.cfg:/app/.bumpversion.cfg \
33+
-v ./pyproject.toml:/app/pyproject.toml \
34+
package:bumpversion
35+
docker rmi package:bumpversion || true

README.md

Lines changed: 110 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
11
# FastAPI Clerk Auth Middleware
22

3+
[![PyPI version](https://img.shields.io/pypi/v/fastapi-clerk-auth.svg)](https://pypi.org/project/fastapi-clerk-auth/)
4+
[![Python Versions](https://img.shields.io/pypi/pyversions/fastapi-clerk-auth.svg)](https://pypi.org/project/fastapi-clerk-auth/)
5+
[![License](https://img.shields.io/github/license/OSSMafia/fastapi-clerk-middleware)](https://github.com/OSSMafia/fastapi-clerk-middleware/blob/main/LICENSE)
36

4-
FastAPI Auth Middleware for [Clerk](https://clerk.com)
7+
A lightweight, easy-to-use authentication middleware for [FastAPI](https://fastapi.tiangolo.com/) that integrates with [Clerk](https://clerk.com) authentication services.
58

6-
Easily setup authentication on your API routes using your Clerk JWKS endpoint.
9+
This middleware allows you to secure your FastAPI routes by validating JWT tokens against your Clerk JWKS endpoint, making it simple to implement authentication in your API.
10+
11+
## Features
12+
13+
- 🔒 Secure API routes with Clerk JWT validation
14+
- 🚀 Simple integration with FastAPI's dependency injection system
15+
- ⚙️ Flexible configuration options (auto error responses, request state access)
16+
- 📝 Decoded token payload accessible in your route handlers
17+
18+
## Installation
719

8-
## Install
920
```bash
1021
pip install fastapi-clerk-auth
1122
```
1223

1324
## Basic Usage
25+
1426
```python
1527
from fastapi import FastAPI, Depends
1628
from fastapi_clerk_auth import ClerkConfig, ClerkHTTPBearer, HTTPAuthorizationCredentials
@@ -19,7 +31,8 @@ from fastapi.encoders import jsonable_encoder
1931

2032
app = FastAPI()
2133

22-
clerk_config = ClerkConfig(jwks_url="https://example.com/.well-known/jwks.json") # Use your Clerk JWKS endpoint
34+
# Use your Clerk JWKS endpoint
35+
clerk_config = ClerkConfig(jwks_url="https://your-clerk-frontend-api.clerk.accounts.dev/.well-known/jwks.json")
2336

2437
clerk_auth_guard = ClerkHTTPBearer(config=clerk_config)
2538

@@ -28,17 +41,99 @@ async def read_root(credentials: HTTPAuthorizationCredentials | None = Depends(c
2841
return JSONResponse(content=jsonable_encoder(credentials))
2942
```
3043

31-
The returned `credentials` model will either be of type `None` or `HTTPAuthorizationCredentials`. If the model is populated it will have the following properties:
32-
- scheme
33-
- Indicates the scheme of the Authorization header (Bearer)
34-
- credentials
35-
- Raw token received from the Authorization header
36-
- decoded
37-
- The payload of the decoded token
44+
The returned `credentials` model will be either `None` or an `HTTPAuthorizationCredentials` object with these properties:
45+
46+
- `scheme`: Indicates the scheme of the Authorization header (Bearer)
47+
- `credentials`: Raw token received from the Authorization header
48+
- `decoded`: The payload of the decoded token
49+
50+
## Configuration Options
51+
52+
### Disabling Auto Errors
53+
54+
By default, the middleware automatically returns 403 errors if the token is missing or invalid. You can disable this behavior:
3855

39-
## Auto Errors
40-
By default the middleware automatically returns 403 errors if the token is missing or invalid. You can disable that behavior by setting the following:
4156
```python
42-
clerk_config = ClerkConfig(jwks_url="https://example.com/.well-known/jwks.json", auto_error=False)
57+
clerk_config = ClerkConfig(
58+
jwks_url="https://your-clerk-frontend-api.clerk.accounts.dev/.well-known/jwks.json",
59+
auto_error=False
60+
)
4361
```
44-
This will allow requests to reach the endpoint for additional logic or error handling.
62+
63+
This allows requests to reach the endpoint for additional logic or custom error handling:
64+
65+
```python
66+
@app.get("/protected")
67+
async def protected_endpoint(credentials: HTTPAuthorizationCredentials | None = Depends(clerk_auth_guard)):
68+
if not credentials:
69+
return {"message": "You're not authenticated, but you can still see this limited data"}
70+
71+
# Full access for authenticated users
72+
return {"message": "Full access granted", "user_data": credentials.decoded}
73+
```
74+
75+
### Adding Auth Data to Request State
76+
77+
You can have the `HTTPAuthorizationCredentials` added to the request state for easier access:
78+
79+
```python
80+
from fastapi import Depends, Request, APIRouter
81+
from fastapi_clerk_auth import ClerkConfig, ClerkHTTPBearer, HTTPAuthorizationCredentials
82+
from fastapi.responses import JSONResponse
83+
from fastapi.encoders import jsonable_encoder
84+
85+
clerk_config = ClerkConfig(
86+
jwks_url="https://your-clerk-frontend-api.clerk.accounts.dev/.well-known/jwks.json",
87+
add_state=True
88+
)
89+
90+
clerk_auth_guard = ClerkHTTPBearer(config=clerk_config)
91+
92+
router = APIRouter(prefix="/todo", dependencies=[Depends(clerk_auth_guard)])
93+
94+
@router.get("/")
95+
async def read_todo_list(request: Request):
96+
auth_data: HTTPAuthorizationCredentials = request.state.clerk_auth
97+
user_id = auth_data.decoded.get("sub")
98+
99+
# Use user_id to fetch the user's todo items
100+
return {"message": f"Todo items for user {user_id}"}
101+
```
102+
103+
## Advanced Usage
104+
105+
### Role-Based Access Control
106+
107+
You can implement role-based access control by checking the JWT claims:
108+
109+
```python
110+
from fastapi import Depends, HTTPException, status
111+
112+
def admin_required(credentials: HTTPAuthorizationCredentials = Depends(clerk_auth_guard)):
113+
if not credentials:
114+
raise HTTPException(
115+
status_code=status.HTTP_401_UNAUTHORIZED,
116+
detail="Not authenticated"
117+
)
118+
119+
user_roles = credentials.decoded.get("roles", [])
120+
if "admin" not in user_roles:
121+
raise HTTPException(
122+
status_code=status.HTTP_403_FORBIDDEN,
123+
detail="Admin permission required"
124+
)
125+
126+
return credentials
127+
128+
@app.get("/admin", dependencies=[Depends(admin_required)])
129+
async def admin_only():
130+
return {"message": "Welcome, admin!"}
131+
```
132+
133+
## Contributing
134+
135+
Contributions are welcome! Please feel free to submit a Pull Request.
136+
137+
## License
138+
139+
This project is licensed under the MIT License - see the LICENSE file for details.

dockerfile

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
FROM python:3.11-slim AS base
2+
WORKDIR /app
3+
COPY ./fastapi_clerk_auth ./fastapi_clerk_auth
4+
COPY ./pyproject.toml ./pyproject.toml
5+
COPY .bumpversion.cfg .bumpversion.cfg
6+
7+
8+
FROM base AS format
9+
COPY ./ruff.toml ./ruff.toml
10+
RUN pip install -e .[dev]
11+
WORKDIR /app/fastapi_clerk_auth
12+
CMD ruff check ./ --fix --config ../ruff.toml && ruff format ./ --config ../ruff.toml
13+
14+
15+
FROM base AS bumpversion
16+
RUN pip install bumpversion
17+
18+
19+
FROM bumpversion AS bump_patch
20+
RUN bump2version patch
21+
22+
23+
FROM bumpversion AS bump_minor
24+
RUN bump2version minor
25+
26+
27+
FROM bumpversion AS bump_major
28+
RUN bump2version major

fastapi_clerk_auth/__init__.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from fastapi import HTTPException
55
from fastapi import Request
6+
from fastapi.encoders import jsonable_encoder
67
from fastapi.openapi.models import HTTPBearer as HTTPBearerModel
78
from fastapi.security import HTTPAuthorizationCredentials as FastAPIHTTPAuthorizationCredentials
89
from fastapi.security import HTTPBearer
@@ -79,12 +80,30 @@ def __init__(
7980
"""
8081
),
8182
] = True,
83+
add_state: Annotated[
84+
bool,
85+
Doc(
86+
"""
87+
By default, the decoded authentication data is returned from the `Depends`
88+
on the route function. If you'd like to have the decoded authentication data
89+
available in the request state, set this to `True`. This is useful when you
90+
want to have the decoded authentication data available in the request state
91+
while applying the middleware to multiple routes via a router dependency.
92+
"""
93+
),
94+
] = False,
8295
debug_mode: bool = False,
8396
):
84-
super().__init__(bearerFormat=bearerFormat, scheme_name=scheme_name, description=description, auto_error=auto_error)
97+
super().__init__(
98+
bearerFormat=bearerFormat,
99+
scheme_name=scheme_name,
100+
description=description,
101+
auto_error=auto_error,
102+
)
85103
self.model = HTTPBearerModel(bearerFormat=bearerFormat, description=description)
86104
self.scheme_name = scheme_name or self.__class__.__name__
87105
self.auto_error = auto_error
106+
self.add_state = add_state
88107
self.config = config
89108
self._check_config()
90109
self.jwks_url: str = config.jwks_url
@@ -99,6 +118,7 @@ def __init__(
99118
headers=config.jwks_headers,
100119
timeout=config.jwks_client_timeout,
101120
)
121+
self.add_state = add_state
102122
self.debug_mode = debug_mode
103123

104124
def _check_config(self) -> None:
@@ -110,7 +130,7 @@ def _check_config(self) -> None:
110130
def _decode_token(self, token: str) -> dict | None:
111131
try:
112132
signing_key = self.jwks_client.get_signing_key_from_jwt(token)
113-
return jwt.decode(
133+
decoded_token = jwt.decode(
114134
token,
115135
key=signing_key.key,
116136
audience=self.audience,
@@ -122,6 +142,7 @@ def _decode_token(self, token: str) -> dict | None:
122142
"verify_iss": self.config.verify_iss,
123143
},
124144
)
145+
return dict(jsonable_encoder(decoded_token))
125146
except Exception as e:
126147
if self.debug_mode:
127148
raise e
@@ -132,23 +153,23 @@ async def __call__(self, request: Request) -> Optional[HTTPAuthorizationCredenti
132153
scheme, credentials = get_authorization_scheme_param(authorization)
133154
if not (authorization and scheme and credentials):
134155
if self.auto_error:
135-
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Not authenticated")
136-
else:
137-
return None
156+
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Not Authenticated")
157+
return None
138158
if scheme.lower() != "bearer":
139159
if self.auto_error:
140160
raise HTTPException(
141161
status_code=HTTP_403_FORBIDDEN,
142-
detail="Invalid authentication credentials",
162+
detail="Invalid Authentication Credentials",
143163
)
144-
else:
145-
return None
164+
return None
146165

147166
decoded_token: dict | None = self._decode_token(token=credentials)
148167
if not decoded_token and self.auto_error:
149168
raise HTTPException(
150169
status_code=HTTP_403_FORBIDDEN,
151-
detail="Invalid authentication credentials",
170+
detail="Invalid Authentication Credentials",
152171
)
153-
154-
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials, decoded=decoded_token)
172+
response = HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials, decoded=decoded_token)
173+
if self.add_state:
174+
request.state.clerk_auth = response
175+
return response

pyproject.toml

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,36 @@
11
[project]
22
name = "fastapi_clerk_auth"
3-
version = "0.0.5"
3+
version = "0.0.6"
44
description = "FastAPI Auth Middleware for Clerk (https://clerk.com)"
55
readme = "README.md"
66
requires-python = ">=3.9"
77
authors = [
88
{ name = "OSS Mafia", email = "dev@oss-mafia.com" },
99
]
10+
classifiers = [
11+
"Development Status :: 5 - Production/Stable",
12+
"Framework :: FastAPI",
13+
"Intended Audience :: Developers",
14+
"License :: OSI Approved :: MIT License",
15+
"Programming Language :: Python :: 3.9",
16+
"Programming Language :: Python :: 3.10",
17+
"Programming Language :: Python :: 3.11",
18+
"Programming Language :: Python :: 3.12",
19+
]
20+
keywords = ["fastapi", "clerk", "authentication", "jwt"]
1021
dependencies = [
1122
"fastapi>=0.95.0",
1223
"PyJWT>=2.0.0",
13-
"cryptography==43.0.1",
24+
"cryptography>=43.0.1",
25+
]
26+
27+
[project.optional-dependencies]
28+
dev = [
29+
"ruff==0.2.0",
30+
"pytest",
31+
"pytest-asyncio",
32+
"pytest-mock",
33+
"bump2version",
1434
]
1535

1636
[project.urls]

requirements-dev.txt

Lines changed: 0 additions & 5 deletions
This file was deleted.

scripts/formatter.sh

Lines changed: 0 additions & 6 deletions
This file was deleted.

scripts/linter.sh

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)