Skip to content

Minimal modern OAuth 2.1 stack - #2296

Open
jeremy wants to merge 4 commits into
mainfrom
oauth
Open

Minimal modern OAuth 2.1 stack#2296
jeremy wants to merge 4 commits into
mainfrom
oauth

Conversation

@jeremy

@jeremy jeremy commented Jan 6, 2026

Copy link
Copy Markdown
Member
  • Builds on existing Identity::AccessToken (optionally belongs to an OAuth client)
  • Provides everything MCP clients need for dynamic registration and auth
  • Adds a Connected Apps listing to user profiles to view and revoke granted tokens

No refresh tokens; no token expiry; authorization code + PKCE only.

Needs design: Authorization

  • Consent screen
oauth consent screen

Needs design: Connected Apps

  • section on user profile
  • listing
  • revocation flow
oauth user profile oauth connected apps

@jeremy
jeremy requested review from dhh and jzimdars January 6, 2026 01:34
Comment thread app/controllers/oauth/base_controller.rb Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 minimal OAuth 2.1-style authorization code + PKCE stack to Fizzy, including dynamic client registration and a “Connected Apps” UI built on the existing Identity::AccessToken model (now optionally associated to an OAuth client).

Changes:

  • Introduces Oauth::Client, encrypted short-lived authorization codes, and OAuth endpoints (authorization, token, revocation, metadata, protected-resource metadata, DCR).
  • Extends Identity::AccessToken to optionally belong to an OAuth client and adds Connected Apps listing + revoke flow under my/connected_apps.
  • Adds model, controller, and integration tests plus fixtures and schema/migration updates.

Reviewed changes

Copilot reviewed 28 out of 29 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
test/models/oauth/client_test.rb Adds unit tests for OAuth client generation and redirect/scope validation
test/models/oauth/authorization_code_test.rb Adds tests for encrypted auth code generation/parsing/PKCE
test/integration/oauth_flow_test.rb End-to-end tests for auth, token, revocation, discovery, and DCR
test/fixtures/oauth/clients.yml Adds OAuth client fixtures used by tests
test/controllers/my/connected_apps_controller_test.rb Tests Connected Apps listing and revocation behavior
db/schema_sqlite.rb Adds oauth_clients table + identity_access_tokens.oauth_client_id (SQLite)
db/schema.rb Adds oauth_clients table + identity_access_tokens.oauth_client_id (+ FK)
db/migrate/20251231163456_add_oauth.rb Migration to create OAuth clients and reference from access tokens
config/routes.rb Wires OAuth endpoints and my/connected_apps routes
app/views/users/show.html.erb Conditionally shows Connected Apps section on profile
app/views/users/_connected_apps.html.erb Adds Connected Apps entry point on profile
app/views/users/_access_tokens.html.erb Minor template adjustment in access token section
app/views/oauth/authorizations/new.html.erb Consent/authorization form for the code flow
app/views/oauth/authorizations/error.html.erb HTML error page when redirect is unsafe/unavailable
app/views/my/connected_apps/index.html.erb Connected Apps listing page
app/views/my/connected_apps/_connected_app.html.erb Row partial with disconnect action
app/models/oauth/client.rb OAuth client model: redirect URI and scope checks
app/models/oauth/authorization_code.rb Encrypted, expiring auth code + PKCE verification
app/models/oauth.rb Defines OAuth namespace + loopback host list
app/models/identity/access_token.rb Adds optional oauth_client + personal/oauth scopes
app/controllers/oauth/tokens_controller.rb Token exchange endpoint for auth code grant
app/controllers/oauth/revocations_controller.rb RFC 7009-style revocation endpoint
app/controllers/oauth/protected_resource_metadata_controller.rb Protected resource metadata (well-known)
app/controllers/oauth/metadata_controller.rb Authorization server metadata (well-known)
app/controllers/oauth/clients_controller.rb Dynamic client registration endpoint
app/controllers/oauth/base_controller.rb Shared OAuth controller behavior and error helper
app/controllers/oauth/authorizations_controller.rb Authorization endpoint + redirect/error handling
app/controllers/my/connected_apps_controller.rb Lists connected clients and revokes tokens per client
app/controllers/my/access_tokens_controller.rb Restricts PAT management to personal (non-OAuth) tokens

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

Comment thread app/views/oauth/authorizations/new.html.erb
Comment thread app/controllers/oauth/tokens_controller.rb
Comment thread db/migrate/20251231163456_add_oauth.rb
Comment thread app/models/oauth/client.rb
Comment thread config/routes.rb
Comment thread app/controllers/oauth/revocations_controller.rb
Comment thread app/models/oauth/client.rb
jeremy added 3 commits August 27, 2026 23:02
- OAuth tokens = Access Token that belongs to an OAuth client
- One table (oauth_clients) + one column (oauth_client_id)
- Stateless authorization codes via MessageEncryptor (60s TTL)
- Implicit grants (token exists = grant, revoke = delete tokens)
- Dynamic client registration for MCPs
- Token lifetime: no expiry, explicit revocation only
- Scope mapping: space-delimited OAuth scopes → permission enum
- Security: PKCE required, loopback-only DCR, rate limiting
- RFC compliance: 6749, 6750, 7636, 7591, 8252, 8414, 9728

Authorization flow with PKCE (S256 only)
- Consent screen showing client name and requested scopes
- Stateless authorization codes via MessageEncryptor

Token endpoint
- Authorization code exchange with PKCE verification
- Issues Identity::AccessToken linked to OAuth client

Revocation endpoint (RFC 7009)
- Revoke access tokens by value
- Always returns 200 per spec
Discovery endpoints (RFC 8414):
- /.well-known/oauth-authorization-server
- /.well-known/oauth-protected-resource

Dynamic Client Registration (RFC 7591):
- POST /oauth/clients for MCP clients
- Loopback redirects only (127.0.0.1, localhost, [::1])
- Rate limited to 10 requests/minute
View and revoke OAuth client access at /my/connected_apps.
Scoped through identity's OAuth tokens for proper authorization.
Links from access tokens index for discoverability.
@jeremy

jeremy commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

Rebased oauth onto current main (738 commits, one trivial conflict in config/routes.rb) and force-pushed: 4a047d8. Full suite green after rebase: 1644 tests, 0 failures.

One follow-up commit landed on this branch during review convergence: 840ec75 accepts form-encoded requests at the token/revocation/registration endpoints — conforming OAuth clients POST application/x-www-form-urlencoded, which the app's forgery protection rejected over https. The endpoints are cookie-free so CSRF doesn't apply; the browser-facing consent form keeps its protection. (Three resulting CodeQL rb/csrf-protection-disabled alerts dismissed with that reasoning.)

Connector-grade gaps closed as a stack of three PRs on top of this branch, one gap each:

  1. Accept https redirect URIs for dynamically registered clients #3080 — https redirect URIs for DCR (hosted connectors), loopback policy otherwise unchanged
  2. Expire OAuth access tokens and rotate refresh tokens #3081 — access-token expiry + refresh tokens with OAuth 2.1 rotation semantics
  3. Support confidential clients with client_secret_post #3082 — confidential clients via client_secret_post, matching what beta2's bc3 AS advertises

All four PRs converged through Copilot/Codex review (17 findings: 15 fixed, 2 declined with reasoning in-thread) and are CI-green. Full suite on the top of the stack: 1683 tests, 0 failures. Merging this PR first, then the stack in order, keeps each diff reviewable.

Still open, deliberately: consent/Connected Apps UI design polish — the screens work (and now name the redirect host for hosted clients) but haven't had a design pass.

Conforming OAuth clients post token, revocation, and registration
requests as application/x-www-form-urlencoded, but the app's forgery
protection rejects headerless non-JSON POSTs over https, answering 422
before the endpoint runs. These are cookie-free server-to-server
endpoints, so CSRF doesn't apply — skip it. The browser-facing
authorization consent form keeps its protection.

Covered by a form-encoded exchange test with forgery protection
active.
Comment thread app/controllers/oauth/clients_controller.rb Dismissed
Comment thread app/controllers/oauth/revocations_controller.rb Dismissed
Comment thread app/controllers/oauth/tokens_controller.rb Dismissed
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