Conversation
979cc45 to
2bbf396
Compare
There was a problem hiding this comment.
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::AccessTokento optionally belong to an OAuth client and adds Connected Apps listing + revoke flow undermy/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.
- 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.
|
Rebased 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 Connector-grade gaps closed as a stack of three PRs on top of this branch, one gap each:
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.
No refresh tokens; no token expiry; authorization code + PKCE only.
Needs design: Authorization
Needs design: Connected Apps