-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from OSSMafia/mnick/setup
feat: init project
- Loading branch information
Showing
14 changed files
with
316 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
[bumpversion] | ||
current_version = 0.0.1 | ||
commit = True | ||
tag = True | ||
|
||
[bumpversion:file:pyproject.toml] | ||
search = version = "{current_version}" | ||
replace = version = "{new_version}" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
name: Publish Python Package | ||
|
||
on: | ||
release: | ||
types: | ||
- created | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- name: Check out the code | ||
uses: actions/checkout@v3 | ||
|
||
- name: Set up Python | ||
uses: actions/setup-python@v4 | ||
with: | ||
python-version: '3.11' | ||
|
||
- name: Install dependencies | ||
run: | | ||
python -m pip install --upgrade pip | ||
pip install build twine | ||
- name: Build the package | ||
run: python -m build | ||
|
||
- name: Publish to PyPI | ||
env: | ||
TWINE_USERNAME: __token__ | ||
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} | ||
run: | | ||
python -m twine upload dist/* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
format: | ||
bash ./scripts/formatter.sh | ||
|
||
lint: | ||
bash ./scripts/linter.sh |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,27 @@ | ||
# fastapi-clerk-middleware | ||
# FastAPI Clerk Auth Middleware | ||
|
||
|
||
FastAPI Auth Middleware for [Clerk](https://clerk.com) | ||
|
||
## Install | ||
```bash | ||
pip install fastapi-clerk | ||
``` | ||
|
||
## Basic Usage | ||
```python | ||
from fastapi import FastAPI, Depends | ||
from fastapi_clerk_auth import ClerkConfig, ClerkHTTPBearer, HTTPAuthorizationCredentials | ||
from fastapi.responses import JSONResponse | ||
from fastapi.encoders import jsonable_encoder | ||
|
||
app = FastAPI() | ||
|
||
clerk_config = ClerkConfig(jwks_url="https://example.com/.well-known/jwks.json") # Use your Clerk JWKS endpoint | ||
|
||
clear_auth_guard = ClerkHTTPBearer(config=clerk_config) | ||
|
||
@app.get("/") | ||
async def read_root(credentials: HTTPAuthorizationCredentials | None = Depends(clear_auth_guard)): | ||
return JSONResponse(content=jsonable_encoder(credentials)) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
from typing import Any | ||
from typing import Optional | ||
|
||
from fastapi import HTTPException | ||
from fastapi import Request | ||
from fastapi.openapi.models import HTTPBearer as HTTPBearerModel | ||
from fastapi.security import HTTPAuthorizationCredentials as FastAPIHTTPAuthorizationCredentials | ||
from fastapi.security import HTTPBearer | ||
from fastapi.security.utils import get_authorization_scheme_param | ||
import jwt | ||
from jwt import PyJWKClient | ||
from pydantic import BaseModel | ||
from starlette.status import HTTP_403_FORBIDDEN | ||
from typing_extensions import Annotated | ||
from typing_extensions import Doc | ||
|
||
|
||
class ClerkConfig(BaseModel): | ||
jwks_url: str | ||
audience: str | None = None | ||
issuer: str | None = None | ||
verify_exp: bool = True | ||
verify_aud: bool = False | ||
verify_iss: bool = False | ||
jwks_cache_keys: bool = False | ||
jwks_max_cached_keys: int = 16 | ||
jwks_cache_set: bool = True | ||
jwks_lifespan: int = 300 | ||
jwks_headers: Optional[dict[str, Any]] = None | ||
jwks_client_timeout: int = 30 | ||
|
||
|
||
class HTTPAuthorizationCredentials(FastAPIHTTPAuthorizationCredentials): | ||
decoded: dict | None = None | ||
|
||
|
||
class ClerkHTTPBearer(HTTPBearer): | ||
def __init__( | ||
self, | ||
config: ClerkConfig, | ||
bearerFormat: Annotated[Optional[str], Doc("Bearer token format.")] = None, | ||
scheme_name: Annotated[ | ||
Optional[str], | ||
Doc( | ||
""" | ||
Security scheme name. | ||
It will be included in the generated OpenAPI (e.g. visible at `/docs`). | ||
""" | ||
), | ||
] = None, | ||
description: Annotated[ | ||
Optional[str], | ||
Doc( | ||
""" | ||
Security scheme description. | ||
It will be included in the generated OpenAPI (e.g. visible at `/docs`). | ||
""" | ||
), | ||
] = None, | ||
auto_error: Annotated[ | ||
bool, | ||
Doc( | ||
""" | ||
By default, if the HTTP Bearer token not provided (in an | ||
`Authorization` header), `HTTPBearer` will automatically cancel the | ||
request and send the client an error. | ||
If `auto_error` is set to `False`, when the HTTP Bearer token | ||
is not available, instead of erroring out, the dependency result will | ||
be `None`. | ||
This is useful when you want to have optional authentication. | ||
It is also useful when you want to have authentication that can be | ||
provided in one of multiple optional ways (for example, in an HTTP | ||
Bearer token or in a cookie). | ||
""" | ||
), | ||
] = True, | ||
debug_mode: bool = False, | ||
): | ||
super().__init__(bearerFormat=bearerFormat, scheme_name=scheme_name, description=description, auto_error=auto_error) | ||
self.model = HTTPBearerModel(bearerFormat=bearerFormat, description=description) | ||
self.scheme_name = scheme_name or self.__class__.__name__ | ||
self.auto_error = auto_error | ||
self.config = config | ||
self._check_config() | ||
self.jwks_url: str = config.jwks_url | ||
self.audience: str | None = config.audience | ||
self.issuer: str | None = config.issuer | ||
self.jwks_client: PyJWKClient = PyJWKClient( | ||
uri=config.jwks_url, | ||
cache_keys=config.jwks_cache_keys, | ||
max_cached_keys=config.jwks_max_cached_keys, | ||
cache_jwk_set=config.jwks_cache_set, | ||
lifespan=config.jwks_lifespan, | ||
headers=config.jwks_headers, | ||
timeout=config.jwks_client_timeout, | ||
) | ||
self.debug_mode = debug_mode | ||
|
||
def _check_config(self) -> None: | ||
if not self.config.audience and self.config.verify_aud: | ||
raise ValueError("Audience must be set in config because verify_aud is True") | ||
if not self.config.issuer and self.config.verify_iss: | ||
raise ValueError("Issuer must be set in config because verify_iss is True") | ||
|
||
def _decode_token(self, token: str) -> dict | None: | ||
try: | ||
signing_key = self.jwks_client.get_signing_key_from_jwt(token) | ||
return jwt.decode( | ||
token, | ||
key=signing_key.key, | ||
audience=self.audience, | ||
issuer=self.issuer, | ||
algorithms=["RS256"], | ||
options={ | ||
"verify_exp": self.config.verify_exp, | ||
"verify_aud": self.config.verify_aud, | ||
"verify_iss": self.config.verify_iss, | ||
}, | ||
) | ||
except Exception as e: | ||
if self.debug_mode: | ||
raise e | ||
return None | ||
|
||
async def __call__(self, request: Request) -> Optional[HTTPAuthorizationCredentials]: | ||
authorization = request.headers.get("Authorization") | ||
scheme, credentials = get_authorization_scheme_param(authorization) | ||
if not (authorization and scheme and credentials): | ||
if self.auto_error: | ||
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Not authenticated") | ||
else: | ||
return None | ||
if scheme.lower() != "bearer": | ||
if self.auto_error: | ||
raise HTTPException( | ||
status_code=HTTP_403_FORBIDDEN, | ||
detail="Invalid authentication credentials", | ||
) | ||
else: | ||
return None | ||
|
||
decoded_token: dict | None = self._decode_token(token=credentials) | ||
if not decoded_token and self.auto_error: | ||
raise HTTPException( | ||
status_code=HTTP_403_FORBIDDEN, | ||
detail="Invalid authentication credentials", | ||
) | ||
|
||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials, decoded=decoded_token) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
[project] | ||
name = "fastapi_clerk_auth" | ||
version = "0.0.1" | ||
description = "FastAPI Auth Middleware for [Clerk](https://clerk.com)" | ||
readme = "README.md" | ||
requires-python = ">=3.9" | ||
authors = [ | ||
{ name = "OSS Mafia", email = "[email protected]" }, | ||
] | ||
dependencies = [ | ||
"fastapi>=0.95.0", | ||
"PyJWT>=2.0.0", | ||
] | ||
|
||
[project.urls] | ||
"Homepage" = "https://github.com/OSSMafia/fastapi-clerk-middleware" | ||
"Source" = "https://github.com/OSSMafia/fastapi-clerk-middleware" | ||
|
||
[tools.setuptools.packages.find] | ||
where = "fastapi_clerk_auth/" | ||
include = ["fastapi_clerk_auth"] | ||
namespaces = true | ||
|
||
[build-system] | ||
requires = ["setuptools", "setuptools-scm"] | ||
build-backend = "setuptools.build_meta" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
ruff==0.2.0 | ||
pytest | ||
pytest-asyncio | ||
pytest-mock | ||
bump2version |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
line-length = 180 | ||
|
||
[lint] | ||
select = [ | ||
"E", # pycodestyle errors | ||
"W", # pycodestyle warnings | ||
"F", # pyflakes | ||
"I", # isort | ||
"B", # flake8-bugbear | ||
"C4", # flake8-comprehensions | ||
"UP", # pyupgrade | ||
] | ||
ignore = [ | ||
"E501", # line too long, handled by black | ||
"B008", # do not perform function calls in argument defaults | ||
"C901", # too complex | ||
"W191", # indentation contains tabs | ||
"B026", # keyword argument unpacking | ||
] | ||
|
||
[lint.isort] | ||
known-first-party = ["fastapi_clerk_auth"] | ||
combine-as-imports = true | ||
force-single-line = true | ||
force-sort-within-sections = true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
#!/bin/sh -e | ||
set -x | ||
|
||
pip install -r requirements-dev.txt | ||
ruff check fastapi_clerk_auth tests --fix | ||
ruff format fastapi_clerk_auth tests |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
#!/bin/sh -e | ||
set -x | ||
|
||
pip install -r requirements-dev.txt | ||
ruff check fastapi_clerk_auth tests |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
#!/bin/sh -e | ||
|
||
set -x | ||
|
||
pip install bump2version | ||
|
||
bump2version major |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
#!/bin/sh -e | ||
|
||
set -x | ||
|
||
pip install bump2version | ||
|
||
bump2version minor |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
#!/bin/sh -e | ||
|
||
set -x | ||
|
||
pip install bump2version | ||
|
||
bump2version patch |