Skip to content

JWT/OIDC backend: add Authorization Code + PKCE support for frontend auth#11463

Merged
nbudin merged 10 commits intomainfrom
jwt-backend-auth
May 9, 2026
Merged

JWT/OIDC backend: add Authorization Code + PKCE support for frontend auth#11463
nbudin merged 10 commits intomainfrom
jwt-backend-auth

Conversation

@nbudin
Copy link
Copy Markdown
Contributor

@nbudin nbudin commented May 9, 2026

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 OAuthApplication model with an is_intercode_frontend flag 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

  • New OAuthApplication model with is_intercode_frontend flag; replaces the raw Doorkeeper::Application class for this app's purposes
  • OAuthApplicationPolicy blocks JWT-authenticated sessions from the OAuth app management UI
  • Doorkeeper configured with skip_authorization for the frontend app and proper OIDC end_session_endpoint
  • CORS setup moved to cors.rb initializer with correct middleware ordering: Executor → FindVirtualHost → Rack::Cors
  • active_storage_svg_sanitizer gem (unmaintained) replaced with an in-app SanitizeSvgJob using Loofah
  • Added gem "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 initialization
  • Added gem "minitest", "~> 5" — minitest 6 (bundled with Ruby 4.0) fully removes Minitest::Mock and the stub extension; migrating to minitest 6 is left as a separate task

🗄 Database Migrations

  • Adds is_intercode_frontend boolean column to oauth_applications with a partial unique index
  • Makes redirect_uri nullable (the frontend app builds URIs dynamically from convention domains)
  • Seeds the "Intercode Frontend" OAuth application on migration

Risks

The OAuthApplication#redirect_uri override returns dynamically-built URIs for the frontend app and calls super for everything else. Existing OAuth apps are unaffected as long as they have a stored redirect_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

nbudin and others added 10 commits May 9, 2026 09:32
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>
@nbudin nbudin marked this pull request as ready for review May 9, 2026 17:46
@nbudin nbudin added enhancement minor Bumps the minor version number on release labels May 9, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 9, 2026

Code Coverage Report: Only Changed Files listed

Package Base Coverage New Coverage Difference
app/controllers/application_controller.rb 🟠 58.65% 🟠 60% 🟢 1.35%
app/graphql/mutations/revoke_authorized_application.rb 🟠 57.14% 🟠 62.5% 🟢 5.36%
app/graphql/types/ability_type.rb 🟢 83.5% 🟢 83.58% 🟢 0.08%
app/graphql/types/mutation_type.rb 🟢 100% 🟢 99.36% 🔴 -0.64%
app/graphql/types/query_type.rb 🟢 77.87% 🟢 78.05% 🟢 0.18%
app/models/oauth_application.rb 🔴 0% 🟠 50% 🟢 50%
app/policies/oauth_application_policy.rb 🔴 0% 🟢 100% 🟢 100%
app/serializers/signup_move_result_serializer.rb 🟠 66.67% 🟠 55.56% 🔴 -11.11%
config/initializers/active_storage.rb 🟢 100% 🟢 83.33% 🔴 -16.67%
config/initializers/cors.rb 🔴 0% 🟠 50% 🟢 50%
config/initializers/doorkeeper.rb 🟢 84% 🟠 71.88% 🔴 -12.12%
config/initializers/doorkeeper_openid_connect.rb 🟠 66.67% 🟢 76.92% 🟢 10.25%
test/factories/oauth_applications.rb 🔴 0% 🟢 100% 🟢 100%
test/policies/oauth_application_policy_test.rb 🔴 0% 🟢 100% 🟢 100%
Overall Coverage 🟢 53.09% 🟢 53.09% ⚪ 0%

Minimum allowed coverage is 0%, this run produced 53.09%

@nbudin nbudin merged commit b04242b into main May 9, 2026
18 checks passed
@nbudin nbudin deleted the jwt-backend-auth branch May 9, 2026 17:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement minor Bumps the minor version number on release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant