A production-grade, DB-less OAuth 2.0 and OpenID Connect 1.0 Authorization Server built with Python / FastAPI and the Authlete API as the backend control plane.
This server provides zero-persistence token and authorization lifecycle management. All cryptographic operations, token issuance, client management, and protocol state are delegated entirely to the Authlete SaaS API — meaning this Python application carries no database and no token-side secrets. It is a pure protocol façade over Authlete.
- Overview
- Architecture & Methodology
- Implemented Endpoints & Capabilities
- Project Structure
- Setup & Execution
This project implements the canonical OAuth 2.0 and OpenID Connect server architecture as specified by the Authlete reference model. The server is intentionally stateless: every API handler receives a request, forwards it to the Authlete backend, and translates the resulting action enum into a correct HTTP response — including status codes, headers, and body.
Core dependencies:
| Component | Technology |
|---|---|
| Web Framework | FastAPI (ASGI) |
| ASGI Server | Uvicorn |
| Authlete SDK | authlete-python |
| HTTP Client (tests) | HTTPX |
| Test Framework | Pytest |
| Runtime | Python ≥ 3.13 |
| Package Manager | uv |
| Containerization | Docker / Docker Compose |
The workspace is organized as two uv sub-packages: python_oauth_server (the server) and compliance_suite (the test harness).
This project follows a strict Test-First, Reference-Verified development methodology. No endpoint is written speculatively. The workflow for every capability is as follows:
Before implementing any Python endpoint, the compliance suite is run against the official java-oauth-server reference implementation (Authlete's canonical Java server, running on http://localhost:8080). This confirms that the test suite itself is correct and that the expected protocol behaviour is well-defined.
TARGET=JAVA pytest compliance_suite/tests/
All tests must pass against the Java reference. The passing test output is recorded and treated as the ground truth specification.
The Python endpoint is then implemented in python_oauth_server/api/, targeting the same Authlete service. The compliance suite is then re-run against the Python server:
TARGET=PYTHON pytest compliance_suite/tests/
The Python server is only considered correct when it achieves an identical green-test result to the Java reference. There is no manual UAT step; compliance is defined purely by the automated suite.
The following table documents every endpoint currently implemented and the RFC or specification it conforms to.
| Category | Endpoint | HTTP Methods | RFC / Specification | Status |
|---|---|---|---|---|
| Core Authorization | /api/authorization |
GET, POST |
RFC 6749 §3.1 | ✅ Implemented |
| Core Authorization | /api/authorization/decision |
POST |
RFC 6749 §4.1 (User Decision Handler) | ✅ Implemented |
| Token Exchange | /api/token |
POST |
RFC 6749 §3.2 | ✅ Implemented |
| Metadata | /.well-known/openid-configuration |
GET |
OpenID Connect Discovery 1.0 | ✅ Implemented |
| Metadata | /api/jwks |
GET |
RFC 7517 (JSON Web Key Set) | ✅ Implemented |
| Token Operations | /api/introspection |
POST |
RFC 7662 (Token Introspection) | ✅ Implemented |
| Token Operations | /api/userinfo |
GET, POST |
OpenID Connect Core §5.3 | ✅ Implemented |
| Token Operations | /api/revocation |
POST |
RFC 7009 (Token Revocation) | ✅ Implemented |
| Advanced Security | /api/par |
POST |
RFC 9126 (Pushed Authorization Requests) | ✅ Implemented |
| AI Agent Provisioning | /api/register |
POST |
RFC 7591 (Dynamic Client Registration) | ✅ Implemented |
| Grant Lifecycle | /api/gm/{grantId} |
GET, DELETE |
RFC 9356 (Grant Management for OAuth 2.0) | ✅ Implemented |
| Federation | /.well-known/openid-federation |
GET |
OpenID Federation 1.0 | ✅ Implemented |
| Federation | /api/federation/register |
POST |
OpenID Federation 1.0 (Explicit Registration) | ✅ Implemented |
| Verifiable Credentials | /.well-known/openid-credential-issuer |
GET |
OID4VCI (Credential Issuer Metadata) | ✅ Implemented |
| Verifiable Credentials | /.well-known/jwt-issuer |
GET |
RFC 8414 / SD-JWT VC (JWT Issuer Metadata) | ✅ Implemented |
| Verifiable Credentials | /api/credential |
POST |
OID4VCI (Credential Endpoint) | ✅ Implemented |
/api/authorization— Implements the complete Authleteactiondispatcher:INTERACTION(renders a Jinja2 login form),LOCATION(302 redirect),NO_INTERACTION(prompt=none), andBAD_REQUEST. Supports bothGETquery string andPOSTform-encoded parameters per RFC 6749./api/token— HandlesBasicauthentication credential extraction from theAuthorizationheader. DispatchesOK,BAD_REQUEST,INVALID_CLIENT, andINTERNAL_SERVER_ERRORactions. The deprecated Resource Owner Password Credentials grant (PASSWORDaction) is intentionally rejected withunsupported_grant_type./api/introspection— Resource Server–authenticated endpoint. Uses a localResourceServerDaofor credential validation before forwarding the token to Authlete's standard introspection API, maintaining strict architectural separation./api/par— Supports bothBasicAuthorization header and form-body credential extraction. Returns201 Createdon success with arequest_urifor subsequent use at/api/authorization./api/register— Accepts a raw JSON body per RFC 7591. Does not require an Initial Access Token to align with thejava-oauth-serverreference configuration./api/gm/{grantId}—GETmaps to theQUERYaction;DELETEmaps to theREVOKEaction. Requires a valid Bearer token in theAuthorizationheader./.well-known/openid-credential-issuer— Delegates toauthlete_api.credentialIssuerMetadata()via aCredentialIssuerMetadataRequestDTO. Returns the OID4VCI Credential Issuer Metadata document describing the Verifiable Credentials this IdP can issue. DispatchesOK(200),NOT_FOUND(404), andINTERNAL_SERVER_ERROR(500) actions./.well-known/openid-federation— Delegates toauthlete_api.federationConfiguration()via aFederationConfigurationRequestDTO. Returns a signed JWT (Entity Statement) representing this IdP's trust metadata. OnOK, the response is served with the spec-requiredapplication/entity-statement+jwtcontent type. DispatchesOK(200),NOT_FOUND(404), andINTERNAL_SERVER_ERROR(500) actions./api/federation/register— Accepts a raw Entity Statement JWT in the request body. Delegates toauthlete_api.federationRegistration()via aFederationRegistrationRequestDTO. Registers the client if the Trust Chain is valid. DispatchesCREATED(201),BAD_REQUEST(400), andINTERNAL_SERVER_ERROR(500) actions./api/credential— OID4VCI Credential Endpoint. Validates the Bearer access token, extracts the JSON credential request payload, and delegates toauthlete_api.credentialSingleIssue()via aCredentialSingleIssueRequestDTO with aCredentialIssuanceOrder. DispatchesOK(200),BAD_REQUEST(400),UNAUTHORIZED(401),FORBIDDEN(403), andINTERNAL_SERVER_ERROR(500) actions./.well-known/jwt-issuer— SD-JWT Issuer Metadata Endpoint. Delegates toauthlete_api.credentialJwtIssuerMetadata()via aCredentialJwtIssuerMetadataRequestDTO. Returns the JWT issuer configuration including signing key references (jwks_uri). DispatchesOK(200),NOT_FOUND(404), andINTERNAL_SERVER_ERROR(500) actions.
authlete-python-project/
├── Dockerfile # Container build (Python 3.13-slim + uv)
├── docker-compose.yml # Single-command server launch on port 8000
├── python_oauth_server/ # The FastAPI application (uv workspace member)
│ ├── main.py # Application entry point; router registration
│ ├── authlete.properties # Authlete service credentials (gitignored)
│ ├── api/
│ │ ├── authorization.py # GET/POST /api/authorization
│ │ ├── authorization_decision.py # POST /api/authorization/decision
│ │ ├── token.py # POST /api/token
│ │ ├── metadata.py # GET /.well-known/openid-configuration, /api/jwks
│ │ ├── userinfo.py # GET/POST /api/userinfo
│ │ ├── introspection.py # POST /api/introspection
│ │ ├── revocation.py # POST /api/revocation
│ │ ├── par.py # POST /api/par (RFC 9126)
│ │ ├── register.py # POST /api/register (RFC 7591)
│ │ ├── gm.py # GET/DELETE /api/gm/{grantId} (RFC 9356)
│ │ ├── federation_configuration.py # GET /.well-known/openid-federation
│ │ ├── federation_registration.py # POST /api/federation/register
│ │ ├── credential_issuer_metadata.py # GET /.well-known/openid-credential-issuer
│ │ ├── credential.py # POST /api/credential (OID4VCI)
│ │ └── jwt_issuer_metadata.py # GET /.well-known/jwt-issuer (RFC 8414)
│ ├── db/
│ │ ├── resource_server_dao.py # In-memory Resource Server credential store
│ │ └── user_dao.py # In-memory user credential store
│ ├── resources/
│ │ ├── resource_servers.json # Resource Server seed data
│ │ └── users.json # User seed data
│ └── templates/
│ └── authorization.html # Jinja2 login/consent form
│
└── compliance_suite/ # Protocol compliance test harness (uv workspace member)
└── tests/
├── conftest.py # TARGET env var routing (JAVA | PYTHON)
├── test_authorization_basics.py
├── test_authorization_decision.py
├── test_authorization_errors.py
├── test_authorization_interaction.py
├── test_token_exchange.py
├── test_metadata.py
├── test_introspection.py
├── test_userinfo.py
├── test_revocation.py
├── test_par.py
├── test_register.py
├── test_grant_management.py
├── test_federation_configuration.py
├── test_federation_registration.py
├── test_credential_issuer_metadata.py
├── test_credential_endpoint.py
└── test_jwt_issuer_metadata.py
- Python 3.13+
uvinstalled (for local development)- Docker installed (for containerized deployment)
- A valid Authlete account with a configured service. Credentials are stored in
python_oauth_server/authlete.properties.
The fastest way to start the server — mirrors the java-oauth-server Docker workflow:
docker compose upThe server will be available at http://localhost:8000. To rebuild after code changes:
docker compose up --buildTo run in detached mode:
docker compose up -d1. Install Dependencies
From the project root, uv resolves and installs all workspace member dependencies using the lockfile:
uv sync2. Run the FastAPI Server
cd python_oauth_server
uv run uvicorn main:app --reload --port 8000The server will be available at http://localhost:8000. The interactive API documentation (Swagger UI) is available at http://localhost:8000/docs.
All test commands must be run from the project root.
Run against the Java reference server (Baseline):
TARGET=JAVA uv run pytest compliance_suite/tests/ -vRequires the
java-oauth-serverto be running onhttp://localhost:8080.
Run against the Python server (Verification):
TARGET=PYTHON uv run pytest compliance_suite/tests/ -vRequires the Python FastAPI server to be running on
http://localhost:8000.
Run a specific test module:
TARGET=PYTHON uv run pytest compliance_suite/tests/test_grant_management.py -vRun with output capture disabled (useful for debugging):
TARGET=PYTHON uv run pytest compliance_suite/tests/ -v -sStatus: Temporarily patched in this codebase. Awaiting upstream fix.
Affected component: authlete-python PyPI package (v1.3.0)
Symptom: When a DELETE /api/gm/{grantId} request results in a successful revocation, the
Authlete V3 backend returns the action string "NO_CONTENT" in its JSON response. The SDK
deserialization in authlete/types/jsonable.py (line 64) attempts:
return attrType[value] # GrantManagementAction['NO_CONTENT']The published SDK is missing two enum members in GrantManagementAction:
| Missing Member | Expected HTTP Status |
|---|---|
NO_CONTENT |
204 No Content (successful DELETE) |
NOT_FOUND |
404 Not Found (grant does not exist) |
This raises KeyError: 'NO_CONTENT', which crashes the ASGI worker and causes the client to see
"Server disconnected without sending a response" rather than a valid 204.
Temporary patch applied: sdk_compat_patch.py is
imported and executed at the very top of main.py, before any API module is loaded. It injects the
two missing enum members directly into the installed GrantManagementAction class at startup using
Python's internal enum mutation APIs (_member_map_, _value2member_map_, _member_names_).
A warning is logged at startup when the patch is active:
SDK COMPAT PATCH applied: Injected missing GrantManagementAction enum members
into authlete v1.3.0: ['NO_CONTENT', 'NOT_FOUND'].
Remove this patch once upstream SDK is updated.
Removal: Delete sdk_compat_patch.py and remove the two import lines at the top of main.py
once the authlete-python package ships a version containing these enum members.
See LICENSE.