Skip to content

Oauth SA#1218

Open
rboixaderg wants to merge 27 commits into
masterfrom
feature/oauth-mcp
Open

Oauth SA#1218
rboixaderg wants to merge 27 commits into
masterfrom
feature/oauth-mcp

Conversation

@rboixaderg

@rboixaderg rboixaderg commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds guillotina.contrib.oauth, a container-scoped OAuth authorization server profile for Guillotina and MCP clients.

The implemented profile is intentionally narrow: OAuth 2.0 Authorization Code with mandatory PKCE (S256) for public clients. Dynamic registration creates public clients only (token_endpoint_auth_method=none) and does not issue client secrets or support confidential-client authentication.

Module organization

The OAuth contrib is organized into focused packages under guillotina/contrib/oauth/:

Package Purpose
api/ Service dispatch (services.py), HTML page rendering (pages.py), and endpoint handlers (endpoints/{authorize,token,register,revoke,consents}).
auth/ OAuth JWT validator, token context, and credential helpers.
discovery/ RFC 8414 authorization server metadata and RFC 9728 protected resource metadata, plus well-known routing.
flow/ Business logic: client validation, consent keys, CSRF tokens, key derivation, PKCE, scopes, and JWT issuance.
indicators/ Extensible resource indicator registry (RFC 8707) with default container resolver and optional MCP resolvers.
integrations/mcp/ MCP-specific audience resolvers, protected-resource metadata provider, and IMCPAuthPolicy for WWW-Authenticate challenge headers.
storage/ IOAuthStore interface, container-scoped store access, PostgreSQL DDL and repository, and periodic cleanup.
utils/ Shared utilities: rate limiting (in-memory + Redis), request parsing, token hashing, URL builders, and error helpers.

OAuth / RFC coverage

Core OAuth profile

  • Adds container-scoped authorization (GET/POST /db/container/oauth/authorize), token (POST /db/container/oauth/token), registration, revocation, consent, and metadata endpoints.
  • Implements the Authorization Code flow for public clients with mandatory PKCE S256.
  • Issues JWT bearer access tokens signed with Guillotina's configured JWT settings.
  • Stores authorization codes, refresh tokens, clients, and remembered consents in PostgreSQL-backed OAuth tables.
  • Uses opaque authorization codes and opaque refresh tokens, stored hashed at rest.
  • Rotates refresh tokens and revokes the refresh-token family on reuse detection.

Relevant RFC alignment:

  • RFC 6749: Authorization Code grant and bearer token response shape.
  • RFC 6750: Bearer token validation for Guillotina API access.
  • RFC 7636: PKCE with verifier/challenge validation and S256 enforcement.
  • RFC 8252: native-app redirect URI handling, including loopback redirects with runtime ports and private-use URI schemes.
  • RFC 9700: OAuth 2.0 security best-current-practice alignment for public clients, PKCE, redirect URI validation, authorization code replay handling, refresh-token rotation, and safer issuer derivation.

Dynamic client registration

  • Adds POST /db/container/oauth/register.
  • Supports public clients only.
  • Server generates client_id; client-supplied client_id is rejected.
  • Validates redirect URIs, grant types, response types, scopes, and token endpoint auth method.
  • Rejects unsafe redirect URIs, fragments, wildcard redirects, and non-loopback plain HTTP redirects.

Relevant RFC alignment:

  • RFC 7591 profile for dynamic registration of public clients only.
  • Uses RFC 7591 error names where applicable, including invalid_redirect_uri, invalid_client_metadata, and unsupported_token_endpoint_auth_method.

Authorization server metadata and issuer handling

  • Adds RFC 8414 authorization server metadata:
    • GET /db/container/.well-known/oauth-authorization-server
    • GET /.well-known/oauth-authorization-server/db/container for issuers with path components.
  • Exposes endpoint metadata, supported grant/response types, PKCE methods, scopes, resource-indicator support, and issuer response support.
  • Does not expose /.well-known/openid-configuration, because this PR does not implement OpenID Connect.

Relevant RFC alignment:

  • RFC 8414: OAuth 2.0 Authorization Server Metadata.
  • RFC 9207: authorization responses include the iss parameter to mitigate authorization server mix-up.

Resource indicators and MCP integration

  • Supports the OAuth resource parameter and preserves repeated resource parameters on authorization requests.
  • Restricts accepted resources to registered resolvers.
  • Registers the container URL as the default resource.
  • When guillotina.contrib.mcp is enabled, registers MCP protocol resources and exposes protected-resource metadata.
  • Adds WWW-Authenticate metadata hints for MCP protected resources.

Relevant RFC alignment:

  • RFC 8707: Resource Indicators for OAuth 2.0.
  • RFC 9728: OAuth 2.0 Protected Resource Metadata.

Revocation and consent management

  • Adds POST /db/container/oauth/revoke for refresh-token revocation.
  • Revoking a refresh token revokes the related refresh-token family and removes remembered consent for that grant.
  • Adds user-facing consent tracking endpoints:
    • GET /db/container/oauth/consents
    • POST /db/container/oauth/consents
  • Consent revocation also revokes active refresh tokens for the user/client pair.

Relevant RFC alignment:

  • RFC 7009: token revocation endpoint for refresh tokens in this public-client profile.
  • RFC 9700: deauthorization hygiene by preventing silent re-issuance after revocation.

Explicitly not included

This PR does not implement:

  • OpenID Connect (id_token, UserInfo, OIDC discovery, OIDC JWKS, subject identifier types, claims negotiation).
  • RFC 7662 token introspection.
  • RFC 7523 JWT bearer assertions or private_key_jwt client authentication.
  • Confidential-client registration or client-secret based token endpoint authentication.
  • RFC 9068 JWT access token profile as a strict interoperable profile; access tokens are JWTs for Guillotina validation, but the PR does not claim full RFC 9068 compliance.

Testing

Local validation run on this branch:

  • flake8 guillotina --config=setup.cfg
  • isort --check-only guillotina/
  • black --check --verbose guillotina
  • mypy --config-file setup.cfg guillotina/
  • DATABASE=postgresql pytest -rfE --reruns 2 guillotina/tests/oauth

The focused PostgreSQL OAuth test run passed with 92 tests.

Notes for reviewers

The main review focus should be the OAuth profile boundaries and security-sensitive behavior:

  • redirect URI validation and native-app redirect handling;
  • PKCE enforcement;
  • authorization-code and refresh-token lifecycle;
  • resource indicator validation;
  • issuer and well-known URL behavior for container-scoped issuers;
  • MCP protected-resource metadata behavior when MCP is enabled.

  • I signed and returned the Plone Contributor Agreement, and received and accepted an invitation to join a team in the Plone GitHub organization.
  • I verified there aren't any other open pull requests for the same change.
  • I followed the guidelines in Contributing to Plone.
  • I successfully ran code quality checks on my changes locally.
  • I successfully ran tests on my changes locally.
  • If needed, I added new tests for my changes.
  • If needed, I added documentation for my changes.
  • I included a change log entry in my commits.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds a new guillotina.contrib.oauth contrib package implementing a container-scoped OAuth 2.0 Authorization Code + PKCE (S256) authorization server profile, with PostgreSQL persistence and optional MCP integration for protected-resource metadata and audience binding.

Changes:

  • Introduces OAuth endpoints (authorize/token/register/revoke/consents) plus RFC 8414 authorization-server metadata and RFC 9207 iss response parameter.
  • Adds PostgreSQL-backed storage (clients, authorization codes, refresh tokens, consents) including periodic cleanup and refresh-token rotation/reuse defense.
  • Integrates MCP with OAuth resource indicators and WWW-Authenticate hints, and adds comprehensive test coverage + docs.

Reviewed changes

Copilot reviewed 52 out of 59 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
guillotina/tests/test_auth.py Adds targeted unit tests for OAuth token signing/validation, issuer safety, resources, and rate limiting.
guillotina/tests/oauth/conftest.py Provides shared OAuth/MCP test settings and helper functions for registration/authorization/token exchange.
guillotina/tests/oauth/test_mcp_oauth.py Exercises MCP protected-resource metadata, challenges, and OAuth audience enforcement for MCP endpoints.
guillotina/tests/oauth/test_oauth_authorize.py Covers /oauth/authorize behavior including PKCE enforcement, redirects, consent/login flows, and resource handling.
guillotina/tests/oauth/test_oauth_consents.py Tests consent listing/revocation endpoints and consent TTL behavior.
guillotina/tests/oauth/test_oauth_metadata.py Validates RFC 8414 metadata behavior and proxy-header handling options.
guillotina/tests/oauth/test_oauth_register.py Tests dynamic registration validation, redirect URI rules, and rate limiting.
guillotina/tests/oauth/test_oauth_revoke.py Tests refresh-token revocation behavior and revoke endpoint rate limiting.
guillotina/tests/oauth/test_oauth_storage_backend.py Verifies storage interface compliance, schema properties, and store-access behavior without PostgreSQL.
guillotina/tests/oauth/test_oauth_store_contract.py Contract tests for repository behavior (clients/codes/refresh/consents) and end-to-end flow with PostgreSQL storage.
guillotina/tests/oauth/test_oauth_token.py Tests token endpoint behavior, rotation/reuse defense, PKCE validation, and token endpoint rate limiting.
guillotina/tests/oauth/test_oauth_validator.py Confirms OAuth access tokens authenticate requests and audience mismatches fail as expected.
guillotina/tests/mcp/test_mcp.py Ensures non-OAuth MCP deployments do not advertise OAuth challenge metadata.
guillotina/contrib/oauth/init.py Declares default OAuth settings, registers services/utilities, and conditionally enables MCP integration.
guillotina/contrib/oauth/install.py Adds addon install/uninstall hooks (including container data cleanup on uninstall).
guillotina/contrib/oauth/interfaces.py Defines IOAuthStorageUtility interface.
guillotina/contrib/oauth/auth/init.py Package marker for OAuth auth components.
guillotina/contrib/oauth/auth/validators.py Implements OAuth JWT bearer validator (audience/issuer/scope checks + request.oauth attachment).
guillotina/contrib/oauth/api/init.py Package marker for OAuth API components.
guillotina/contrib/oauth/api/request.py Adds request parsing/helpers (duplicate param rejection, form parsing, client identifier, writable GET handling).
guillotina/contrib/oauth/api/services.py Implements OAuth endpoints, well-known handlers, consent flows, rate limiting, and token issuance/rotation.
guillotina/contrib/oauth/api/urls.py Implements issuer/container URL derivation, issuer validation, well-known URL builders, and resource validation.
guillotina/contrib/oauth/api/views.py Renders login/consent/error HTML pages with CSP anti-framing headers.
guillotina/contrib/oauth/api/well_known.py Implements RFC-style well-known routes that map root well-known paths to container-scoped issuers/resources.
guillotina/contrib/oauth/api/templates/base.html Base HTML template for OAuth UI pages.
guillotina/contrib/oauth/api/templates/consent.html Consent page template.
guillotina/contrib/oauth/api/templates/error.html Error page template.
guillotina/contrib/oauth/api/templates/hidden_input.html Template for rendering hidden form inputs.
guillotina/contrib/oauth/api/templates/list_item.html Template for list item rendering.
guillotina/contrib/oauth/api/templates/login.html Login form template.
guillotina/contrib/oauth/api/templates/oauth.css Styling for OAuth HTML UI pages.
guillotina/contrib/oauth/api/templates/plain_item.html Template for plain list items.
guillotina/contrib/oauth/api/templates/scope_item.html Template for scope list items with descriptions.
guillotina/contrib/oauth/flow/init.py Package marker for OAuth flow logic.
guillotina/contrib/oauth/flow/clients.py Implements dynamic client validation/creation, redirect URI rules, and consent key derivation.
guillotina/contrib/oauth/flow/csrf.py Implements signed CSRF tokens for consent decisions with TTL checking.
guillotina/contrib/oauth/flow/keys.py Provides purpose-specific key derivation from jwt.secret for domain separation.
guillotina/contrib/oauth/flow/pkce.py Implements PKCE verifier/challenge validation and S256 verification.
guillotina/contrib/oauth/flow/ratelimit.py Adds best-effort sliding-window rate limiting with optional Redis backend.
guillotina/contrib/oauth/flow/resources.py Implements extensible OAuth resource/audience resolvers (container default + optional MCP).
guillotina/contrib/oauth/flow/scopes.py Defines default scope and configured supported scopes.
guillotina/contrib/oauth/flow/tokens.py Issues JWT access tokens and provides opaque token generation + hashed-at-rest token hashing.
guillotina/contrib/oauth/integrations/init.py Package marker for OAuth integrations.
guillotina/contrib/oauth/integrations/mcp.py Registers MCP resource/audience resolvers and protected-resource well-known metadata + MCP auth policy.
guillotina/contrib/oauth/storage/init.py Package marker for OAuth storage.
guillotina/contrib/oauth/storage/access.py Resolves container DB key, addon installation status, and returns the container-scoped OAuth store.
guillotina/contrib/oauth/storage/interfaces.py Defines the IOAuthStore interface contract.
guillotina/contrib/oauth/storage/utility.py Initializes OAuth tables and runs periodic cleanup for PostgreSQL storage.
guillotina/contrib/oauth/storage/pg/init.py Package marker for PostgreSQL OAuth storage.
guillotina/contrib/oauth/storage/pg/repository.py Implements PostgreSQL OAuth repository (clients/codes/refresh/consents + cleanup function call).
guillotina/contrib/oauth/storage/pg/schema.py Defines PostgreSQL DDL for OAuth tables/indexes and cleanup function.
guillotina/contrib/mcp/interfaces.py Adds IMCPAuthPolicy interface to support optional auth challenge behavior.
guillotina/contrib/mcp/services.py Adjusts MCP protocol endpoint to be Public and enforce auth/permission with optional OAuth challenge headers.
guillotina/auth/validators.py Ensures generic JWT validators ignore OAuth access tokens and uses broader PyJWT exception handling.
docs/source/contrib/oauth.md Adds end-user/operator documentation for configuration and OAuth flows (including MCP behavior).
docs/source/contrib/index.rst Adds OAuth docs to contrib index.
docs/source/_static/oauth-flow.svg Adds an OAuth/MCP flow diagram.
CHANGELOG.rst Adds changelog entries describing OAuth functionality and security posture.
.gitignore Adds /.venv to ignored paths.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread guillotina/contrib/oauth/auth/validators.py
Comment thread guillotina/contrib/oauth/storage/utility.py
Comment thread guillotina/contrib/oauth/storage/pg/repository.py
Comment thread docs/source/contrib/oauth.md
Phases 0-9 implemented: OAuth contrib skeleton, container-scoped annotation storage (.oauth), metadata, dynamic registration, authorization/consent UI, token/refresh/revoke endpoints, OAuth JWT validator, MCP protected resource metadata/challenges, MCP scope-aware cache keys, docs and tests.\n\nValidation attempted:\n- .venv/bin/pytest guillotina/tests/test_login.py guillotina/tests/mcp/test_mcp.py -> failed: .venv/bin/pytest not found in worktree.\n- /Users/rogerboixaderguell/.pyenv/versions/Guillotina/bin/python -m pytest guillotina/tests/oauth/test_oauth_metadata.py -q -> failed during pytest startup: pytest_asyncio has no attribute fixture.\n- python3 -m compileall -q guillotina/contrib/oauth guillotina/contrib/mcp guillotina/tests/oauth -> passed.\n- git diff --check -> passed.
Continue OAuth/MCP phases after creating the worktree virtual environment.\n\nChanges:\n- Consolidate OAuth routes under oauth/{action} and .well-known/{action} to avoid Guillotina route-prefix conflicts.\n- Serve MCP protected resource metadata from the OAuth well-known dispatcher, preserving OAuth -> MCP independence by avoiding MCP imports.\n- Allow GET /oauth/authorize to write authorization codes after remembered consent.\n- Return redirects instead of raising them so authorization-code persistence commits.\n- Use UTC-safe JWT timestamps.\n- Make core JWT validators ignore PyJWT audience errors so OAuth audience rejection does not become a 500.\n- URL-encode OAuth test form bodies.\n- Ignore local .venv.\n\nValidation:\n- .venv/bin/pytest guillotina/tests/oauth -q -> 24 passed, 11 warnings.\n- .venv/bin/pytest guillotina/tests/mcp/test_mcp.py guillotina/tests/test_login.py -q -> 18 passed, 8 warnings.\n- .venv/bin/pytest guillotina/tests/oauth guillotina/tests/mcp/test_mcp.py guillotina/tests/test_login.py -q -> 42 passed, 19 warnings.
Finish remaining OAuth/MCP phase work.\n\nChanges:\n- Keep OAuth independent of MCP by making OAuth expose a generic well-known dispatcher and letting MCP register its protected-resource metadata provider.\n- Add MCP preflight OAuth scope checks for search and serialized content paths so insufficient scopes return HTTP 403.\n- Replay request bodies after preflight inspection so MCP streamable HTTP handling still receives the JSON-RPC payload.\n- Add validation coverage for expired authorization codes, expired refresh tokens, MCP insufficient search scope, and MCP insufficient content scope.\n- Black-format OAuth/MCP touched files.\n\nValidation:\n- .venv/bin/pytest guillotina/tests/oauth guillotina/tests/mcp/test_mcp.py guillotina/tests/test_login.py -q -> 46 passed, 24 warnings.\n- git diff --check -> passed.
- Add configurable consent_ttl (default 30 days, 0 disables expiry) with an
  expires_at column on oauth_consents, purged by oauth_cleanup_expired.
- Expose GET/POST /oauth/consents for authenticated users to list and revoke
  their own grants (401 for anonymous, 404 for unknown consent_key).
- Drop the stored consent on revocation (consent endpoint or refresh-token
  /oauth/revoke) and clear all refresh tokens for the (user, client) pair so a
  grant can no longer be re-issued silently.
- Add IOAuthStore methods list_consents, delete_consent and
  revoke_user_client_refresh_tokens; renew expires_at on consent re-grant.
- Add oauth consents tests and visual documentation (oauth-overview.html).
- Added validation for required parameters in OAuth token, authorization code, refresh token, and revoke endpoints, returning appropriate HTTPBadRequest responses for missing or invalid requests.
- Improved handling of protected resource paths in MCP integration, ensuring correct URL generation and validation.
- Updated tests to cover new validation scenarios and ensure compliance with the updated OAuth flow.
- Removed the outdated oauth-overview.html file to streamline documentation.
- Updated oauth.md to reflect changes in OAuth 2.0 Authorization Code + PKCE configuration.
- Improved request parameter handling in the OAuth API to preserve multiple resource parameters.
- Enhanced validation for redirect URIs, including support for loopback and private-use native redirects.
- Added tests for new functionality, ensuring compliance with updated OAuth flows and parameter handling.
…user and improve anonymous user handling across OAuth endpoints
…odules, updating timestamp handling, and clarifying write permissions for GET requests
@rboixaderg rboixaderg requested a review from nilbacardit26 June 16, 2026 19:20
…cluding access token validation, audience resolution, and rate limiting
…andling, streamline validation processes, and enhance integration with MCP
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants