Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions .agents
Original file line number Diff line number Diff line change
Expand Up @@ -311,15 +311,17 @@ open docs/_build/index.html
- `cachetools` (≥4.1.1) - Caching

### Development Dependencies
- `pytest` (^8.0.0) - Testing framework
- `pytest-cov` (^5.0.0) - Coverage reporting
- `black` (^24.0.0) - Code formatter
- `mypy` (^1.11.0) - Type checker
- `pre-commit` (^3.0.0) - Git hooks
- `pytest` - Testing framework
- `pytest-cov` - Coverage reporting
- `black` - Code formatter
- `mypy` - Type checker
- `pre-commit` - Git hooks

Exact constraints live in `pyproject.toml`; do not duplicate them here.

## Version History

**Current Version:** 0.0.11
**Current Version:** see `pyproject.toml` (`poetry version --short`)

**Recent Changes:**
- Added py.typed for PEP 561 compliance
Expand Down Expand Up @@ -360,5 +362,5 @@ open docs/_build/index.html

---

Last Updated: 2026-01-15
Last Updated: 2026-09-07
Version: 1.0.0
2 changes: 0 additions & 2 deletions .env

This file was deleted.

3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Copy to .env (gitignored). Only needed for `task publish`; releases normally go
# through the Release GitHub workflow, which needs no token.
PYPI_TOKEN=
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ updates:
labels:
- "dependencies"
- "python"
groups:
# One PR per week for routine bumps instead of one PR per package.
# Major versions stay separate so breaking changes get their own review.
python-minor-and-patch:
update-types:
- "minor"
- "patch"

- package-ecosystem: "github-actions"
directory: "/"
Expand All @@ -17,3 +24,7 @@ updates:
labels:
- "dependencies"
- "github-actions"
groups:
github-actions:
patterns:
- "*"
55 changes: 55 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Build and publish to PyPI when a GitHub release is published.
#
# Uses PyPI Trusted Publishing (OIDC), so no API token is stored in this repo.
# One-time setup on PyPI: project fastapi-oidc -> Settings -> Publishing ->
# add a GitHub publisher with owner HarryMWinters, repository fastapi-oidc,
# workflow release.yaml, environment pypi.
name: Release

on:
release:
types: [published]
workflow_dispatch:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prevent manual dispatch from bypassing release validation

When this workflow is started through workflow_dispatch, the build job skips the version/tag guard because that step only runs for release events, but the publish job still uploads to PyPI unconditionally. A maintainer can therefore select any branch or ref and create an irreversible PyPI release without a corresponding validated tag; remove the manual trigger or require and validate a release tag before allowing the publish job to run.

Useful? React with 👍 / 👎.


permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- uses: snok/install-poetry@v1
- name: Check that the tag matches the package version
if: github.event_name == 'release'
run: |
pkg="$(poetry version --short)"
tag="${GITHUB_REF_NAME#v}"
if [ "$pkg" != "$tag" ]; then
echo "pyproject.toml version ($pkg) does not match release tag ($tag)" >&2
exit 1
fi
- run: poetry build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
if-no-files-found: error

publish:
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/fastapi-oidc/
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
11 changes: 11 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ on:
types: [opened, synchronize]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
Expand All @@ -25,6 +32,10 @@ jobs:

- name: Install Task
uses: arduino/setup-task@v3
with:
# Authenticated GitHub API calls avoid the anonymous rate limit that
# intermittently fails this step on busy runners.
repo-token: ${{ secrets.GITHUB_TOKEN }}

- name: Install Poetry
uses: snok/install-poetry@v1
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,4 @@ dmypy.json

# Cython debug symbols
cython_debug/
.serena/
18 changes: 9 additions & 9 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v6.0.0
hooks:
- id: check-merge-conflict
- id: check-yaml
Expand All @@ -11,18 +11,18 @@ repos:
- id: check-added-large-files

- repo: https://github.com/pycqa/isort
rev: 5.13.2
rev: 9.0.1
hooks:
- id: isort
name: isort (python)

- repo: https://github.com/psf/black
rev: 24.4.2
rev: 26.5.1
hooks:
- id: black

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.11.0
rev: v2.3.1
hooks:
- id: mypy
additional_dependencies:
Expand All @@ -32,19 +32,19 @@ repos:
- "types-cachetools"

- repo: https://github.com/pycqa/flake8
rev: "7.1.0"
rev: "7.3.0"
hooks:
- id: flake8

- repo: https://github.com/PyCQA/bandit
rev: "1.7.9"
rev: "1.9.4"
hooks:
- id: bandit
entry: bandit -c .bandit.yml

- repo: https://github.com/python-poetry/poetry
rev: 1.8.0
rev: 2.4.1
hooks:
- id: poetry-check
- id: poetry-lock
args: ["--check"]
# Poetry 2 folded `poetry lock --check` into `poetry check --lock`.
args: ["--lock"]
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- `Release` GitHub workflow that builds and publishes to PyPI via Trusted
Publishing when a GitHub release is published (no PyPI token in the repo).
It refuses to publish if the release tag does not match the package version.
- Dependabot now groups minor/patch updates into one weekly PR per ecosystem.
- Tests for JWKS fetching: HTTP errors propagate and results are cached by URI.

### Fixed
- The JWKS fetch in `discovery.py` now calls `raise_for_status()`, so an error
response from the provider is raised instead of being parsed and cached as a
key set.
- `OIDCConfig.signature_cache_ttl` is typed as `int` (it was `str`).

### Changed
- **Replaced `python-jose` with `PyJWT` for token verification.** `python-jose`
pulls in `ecdsa`, `rsa` and `pyasn1`, which have carried unfixable or
Expand All @@ -19,6 +32,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
header. A JWKS with a single key is still accepted for tokens without a `kid`.
- The minimum PyJWT version is 2.13.0, which includes fixes for algorithm
allow-list bypass and key-confusion issues when verifying with JWK keys.
- Refreshed the dependency lock: cryptography 46.0.7 → 50.0.1 (clears four
advisories), fastapi 0.137 → 0.141, starlette 1.3 → 1.6, pydantic 2.13.5,
requests 2.34, cachetools 7.1.
- Dev tooling: pre-commit 4.x, pylint 4.x, uvicorn 0.52, `httpx2` replaces
`httpx` for `TestClient` (Starlette 1.3+ deprecates `httpx` there).
- Pre-commit hook pins updated to match the Poetry-managed tool versions
(black 26.5, isort 9, mypy 2.3, flake8 7.3, bandit 1.9, poetry 2.4). The
`poetry-lock --check` hook became `poetry-check --lock` (Poetry 2 syntax).
- CI: the test workflow declares read-only `permissions`, cancels superseded
runs of the same ref, and passes a token to `setup-task` so it stops hitting
the anonymous GitHub API rate limit.
- `task publish` now expects a PyPI API token in `PYPI_TOKEN` instead of a
username/password pair, which PyPI no longer accepts. `.env` is no longer
tracked; copy `.env.example` instead.

### Removed
- `python-jose[cryptography]` runtime dependency and the `types-python-jose`
Expand Down
27 changes: 27 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,33 @@ poetry run bandit -r fastapi_oidc
- Fill out the pull request template
- Submit for review

## Releasing (maintainers)

Releases are published to PyPI by the `Release` GitHub workflow using
[Trusted Publishing](https://docs.pypi.org/trusted-publishers/), so no PyPI
token lives in this repository or in CI secrets.

1. Bump the version and move the `Unreleased` changelog entries under it:
```bash
poetry version <major|minor|patch>
```
Update `fastapi_oidc/__init__.py` (`__version__`) and `docs/conf.py`
(`release`) to the same value, then merge that PR.

2. Create a GitHub release whose tag is `v<version>` (for example `v0.2.0`).
Publishing the release triggers the workflow. It builds the sdist and wheel,
checks that the tag matches `pyproject.toml`, and uploads to PyPI.

One-time setup, if the PyPI project has not been linked yet: on
[pypi.org](https://pypi.org/manage/project/fastapi-oidc/settings/publishing/)
add a GitHub publisher with owner `HarryMWinters`, repository `fastapi-oidc`,
workflow `release.yaml`, environment `pypi`. Then create a `pypi` environment
under the repository's Settings → Environments (optionally with required
reviewers).

`task publish` still works as a manual fallback; it needs a PyPI API token in
`PYPI_TOKEN` (see `.env.example`).

## Coding Standards

### General Guidelines
Expand Down
13 changes: 2 additions & 11 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,10 @@ tasks:
- dist/**

publish:
desc: "Push package to PIPy"
desc: "Publish to PyPI. Prefer the Release GitHub workflow (trusted publishing); this needs a PyPI API token in PYPI_TOKEN."
deps: [build]
cmds:
- echo "Publishing as $PYPI_USERNAME"
- poetry publish -u $PYPI_USERNAME -p $PYPI_PASSWORD
# sources:
# # TODO This is erroneously registering as up to date
# # After a build has been run :( )
# - dist/**
# - poetry.lock
# - pyproject.toml
# status:
# - echo "Return nonzero code if we shouldn't publish"
- poetry publish --username __token__ --password "$PYPI_TOKEN"

clean:
desc: "Remove artifacts and caches."
Expand Down
5 changes: 4 additions & 1 deletion fastapi_oidc/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def configure(*_, cache_ttl: int):

@cached(TTLCache(1, cache_ttl), key=lambda d: d["jwks_uri"])
def get_authentication_server_public_keys(
OIDC_spec: dict[str, Any]
OIDC_spec: dict[str, Any],
) -> dict[str, Any]:
"""Retrieve the public keys used by the authentication server.

Expand All @@ -39,10 +39,13 @@ def get_authentication_server_public_keys(
Dictionary containing the public keys in JWKS format.

Raises:
requests.HTTPError: If the JWKS endpoint returns an error status.
requests.RequestException: If the request to fetch keys fails.
"""
keys_uri = OIDC_spec["jwks_uri"]
r = requests.get(keys_uri, timeout=15)
# Without this a 4xx/5xx body would be parsed as (and cached as) a JWKS.
r.raise_for_status()
keys = r.json()
return keys

Expand Down
2 changes: 1 addition & 1 deletion fastapi_oidc/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class OIDCConfig(BaseModel):
client_id: str
base_authorization_server_uri: str
issuer: str | Iterable[str]
signature_cache_ttl: str
signature_cache_ttl: int


class IDToken(BaseModel):
Expand Down
Loading
Loading