JWT/OIDC backend: add Authorization Code + PKCE support for frontend auth#11463
Merged
JWT/OIDC backend: add Authorization Code + PKCE support for frontend auth#11463
Conversation
Introduces the backend infrastructure needed for the frontend to authenticate via OpenID Connect (Authorization Code + PKCE flow) with JWT access tokens, as a prerequisite to migrating the frontend off cookie-based sessions. Key changes: - Add OAuthApplication model with is_intercode_frontend flag and dynamic redirect URI logic across all convention domains - Add OAuthApplicationPolicy blocking JWT-authenticated sessions from managing OAuth apps - Configure Doorkeeper to use OAuthApplication, redirect unauthenticated OAuth requests to login, and skip the authorization prompt for the frontend app - Enable end_session_endpoint and force HTTPS protocol in the OpenID Connect config - Add rack-cors and configure CORS for GraphQL, authenticity tokens, and sign-out endpoints - Allow JWT-authenticated requests to execute GraphQL mutations (skip CSRF check when doorkeeper_token is present) - Support X-Intercode-Convention-Domain header in virtual host lookup - Add migrations for is_intercode_frontend column and nullable redirect_uri - Rename doorkeeper_applications factory to oauth_applications; add OAuthApplicationPolicy tests - Upgrade RuboCop to >= 1.82 for Ruby 4.0 support; add rubocop_todo.yml for pre-existing offenses in modified files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two separate Ruby 4.0 compatibility issues were preventing the server from starting, both pre-existing on main: 1. ActiveStorage::Engine.config.active_storage.content_types_to_serve_as_binary returned nil when active_storage.rb initializer ran (engine defaults not yet applied). Fixed by deferring the delete call to after_initialize and accessing ActiveStorage.content_types_to_serve_as_binary directly. 2. active_storage_svg_sanitizer (unmaintained) triggered a load-order crash in Ruby 4.0: its to_prepare callback loaded ActiveStorage::Blob before ActiveRecord::Base had the has_one_attached extension. Replaced the gem with an in-app equivalent: - SanitizeSvgJob (same Loofah-based script-stripping logic) - ActiveSupport.on_load(:active_storage_blob) hook in the initializer Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add benchmark gem (removed from Ruby 4.0 default gems, but required by mini_magick; its absence caused a silent LoadError that killed ActiveStorage engine initialization, making has_one_attached unavailable on all models) - Remove now-unnecessary ActiveStorage init workarounds in application.rb (they were masking the root cause) - Set up CORS middleware in cors.rb using insert_after so the order is ActionDispatch::Executor → FindVirtualHost → Rack::Cors; also move FindVirtualHost insertion to application.rb with insert_after Executor - Fix migration to use Doorkeeper::OAuth::Scopes#to_s instead of #join - Add frozen_string_literal comment to application.rb Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
minitest 6 (bundled with Ruby 4.0) fully removes Minitest::Mock and the stub extension - migration to minitest 6 is a separate effort. Pin to 5.x for now. Fix policy scope test to use OAuthApplication.all instead of Doorkeeper::Application.all since the factory creates OAuthApplication instances. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
minitest 6 (bundled with Ruby 4.0) extracted mock support to a separate minitest-mock gem. Adding it explicitly ensures mock/stub functionality is available without needing to pin the minitest version itself. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…licy Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Contributor
Code Coverage Report: Only Changed Files listed
Minimum allowed coverage is |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
This is the backend half of a larger change to move Intercode's frontend off cookie-based session auth and onto JWT tokens stored in localStorage, using an OpenID Connect Authorization Code + PKCE flow. The frontend side lives on a separate branch; this PR makes the backend ready to support it without changing any existing behavior.
The main additions: a Doorkeeper OIDC Authorization Code flow for a designated "Intercode Frontend" OAuth application, a custom
OAuthApplicationmodel with anis_intercode_frontendflag and dynamic redirect URI generation (since the frontend can be on any convention domain), and a policy that prevents JWT-authenticated sessions from managing OAuth apps via the UI (to avoid confused deputy issues).Changes
💻 Engineer-facing
OAuthApplicationmodel withis_intercode_frontendflag; replaces the rawDoorkeeper::Applicationclass for this app's purposesOAuthApplicationPolicyblocks JWT-authenticated sessions from the OAuth app management UIskip_authorizationfor the frontend app and proper OIDCend_session_endpointcors.rbinitializer with correct middleware ordering:Executor → FindVirtualHost → Rack::Corsactive_storage_svg_sanitizergem (unmaintained) replaced with an in-appSanitizeSvgJobusing Loofahgem "benchmark"— removed from Ruby 4.0 default gems but required by mini_magick; its absence caused a silent LoadError that killed the entire ActiveStorage engine initializationgem "minitest", "~> 5"— minitest 6 (bundled with Ruby 4.0) fully removesMinitest::Mockand thestubextension; migrating to minitest 6 is left as a separate task🗄 Database Migrations
is_intercode_frontendboolean column tooauth_applicationswith a partial unique indexredirect_urinullable (the frontend app builds URIs dynamically from convention domains)Risks
The
OAuthApplication#redirect_urioverride returns dynamically-built URIs for the frontend app and callssuperfor everything else. Existing OAuth apps are unaffected as long as they have a storedredirect_uri, which the null migration doesn't touch.JWT-authenticated requests can no longer reach the OAuth app management UI — this is intentional but worth verifying doesn't break any existing automation.
Testing
Manually tested the existing OAuth consumer app Authorization Code flow end-to-end. Policy tests pass. The branch is running against Ruby 4.0.3 + Rails 8.1.1.
🚢
🤖 Generated with Claude Code