Skip to content

Commit 89788ab

Browse files
committed
TDD step 6: Rails engine + dummy app + OmniAuth wiring
Registers the gem's OmniauthCallbacksController via a singleton_class prepend on ActiveAdmin::Devise inside a to_prepare callback so the patch applies after ActionDispatch has set up controller autoloading. Adds a minimal dummy Rails app under spec/dummy for request specs.
1 parent 056d016 commit 89788ab

27 files changed

Lines changed: 436 additions & 35 deletions

Gemfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,8 @@
33
source "https://rubygems.org"
44

55
gemspec
6+
7+
# Pin Rails for the dev/test environment. The gemspec allows >= 7.2 at
8+
# runtime, but the dummy app and specs are validated against 7.2.
9+
gem "rails", "~> 7.2.0"
10+
gem "activerecord", "~> 7.2.0"

activeadmin-oidc.gemspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ Gem::Specification.new do |spec|
3939
spec.add_development_dependency "webmock", ">= 3.19"
4040
spec.add_development_dependency "jwt", ">= 2.7"
4141
spec.add_development_dependency "sqlite3", ">= 1.7"
42+
spec.add_development_dependency "sprockets-rails", ">= 3.4"
43+
spec.add_development_dependency "sassc-rails", ">= 2.1"
4244
spec.add_development_dependency "rake", ">= 13.0"
4345
spec.add_development_dependency "rubocop", ">= 1.60"
4446
spec.add_development_dependency "rubocop-rails", ">= 2.20"
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# frozen_string_literal: true
2+
3+
require "devise"
4+
5+
module ActiveAdmin
6+
module Oidc
7+
module Devise
8+
# Handles the OAuth callback from the IdP. Wiring:
9+
#
10+
# # config/routes.rb (or inside ActiveAdmin::Devise.config)
11+
# devise_for :admin_users, ActiveAdmin::Devise.config.merge(
12+
# controllers: {
13+
# omniauth_callbacks: "active_admin/oidc/devise/omniauth_callbacks"
14+
# }
15+
# )
16+
#
17+
# The action name matches the provider name registered with Devise
18+
# (`:oidc`, from ActiveAdmin::Oidc::Engine::PROVIDER_NAME).
19+
class OmniauthCallbacksController < ::Devise::OmniauthCallbacksController
20+
def oidc
21+
auth = request.env["omniauth.auth"] || {}
22+
info = auth["info"] || {}
23+
extra = auth.dig("extra", "raw_info") || {}
24+
25+
claims = extra.to_h.transform_keys(&:to_s)
26+
claims["sub"] = auth["uid"] if claims["sub"].blank? && auth["uid"].present?
27+
claims["email"] = info["email"] if claims["email"].blank? && info["email"].present?
28+
29+
admin_user = UserProvisioner.new(
30+
ActiveAdmin::Oidc.config,
31+
claims: claims,
32+
provider: ActiveAdmin::Oidc::Engine::PROVIDER_NAME.to_s
33+
).call
34+
35+
sign_in_and_redirect admin_user, event: :authentication
36+
set_flash_message(:notice, :success, kind: "OIDC") if is_navigational_format?
37+
rescue ActiveAdmin::Oidc::ProvisioningError => e
38+
Rails.logger.warn("[activeadmin-oidc] denial: #{e.message}")
39+
flash[:alert] = ActiveAdmin::Oidc.config.access_denied_message
40+
redirect_to after_omniauth_failure_path_for(resource_name)
41+
end
42+
43+
def failure
44+
Rails.logger.warn("[activeadmin-oidc] omniauth failure: #{failure_message}")
45+
flash[:alert] = ActiveAdmin::Oidc.config.access_denied_message
46+
redirect_to after_omniauth_failure_path_for(resource_name)
47+
end
48+
end
49+
end
50+
end
51+
end

lib/activeadmin-oidc.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,14 @@ def reset!
3030
require "activeadmin/oidc/discovery"
3131
require "activeadmin/oidc/presets/zitadel"
3232
require "activeadmin/oidc/user_provisioner"
33+
require "rails/engine"
34+
require "activeadmin/oidc/engine"
35+
require "activeadmin/oidc/omniauth_options"
36+
37+
module ActiveAdmin
38+
module Oidc
39+
def self.omniauth_options
40+
OmniauthOptions.build(config)
41+
end
42+
end
43+
end

lib/activeadmin/oidc/engine.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# frozen_string_literal: true
2+
3+
require "rails/engine"
4+
5+
module ActiveAdmin
6+
module Oidc
7+
# Mountable Rails engine. Ships the OmniauthCallbacksController the host
8+
# app's Devise setup routes to, and exposes the hardcoded callback path
9+
# all hosts share.
10+
class Engine < ::Rails::Engine
11+
# OmniAuth strategy name. The callback URL in the host app is
12+
# <host>/admin/auth/oidc/callback
13+
# (ActiveAdmin mounts Devise under /admin by default.) Zitadel and
14+
# other IdPs support multiple redirect URIs per app, so hardcoding
15+
# this path keeps config surface small with no real downside.
16+
PROVIDER_NAME = :oidc
17+
18+
# Prepended into ActiveAdmin::Devise's singleton class so that
19+
# `ActiveAdmin::Devise.controllers` includes the gem's omniauth
20+
# callbacks controller without any host-app wiring.
21+
ControllersPatch = Module.new do
22+
def controllers
23+
super.merge(
24+
omniauth_callbacks: "active_admin/oidc/devise/omniauth_callbacks"
25+
)
26+
end
27+
end
28+
29+
# Apply the patch once ActiveAdmin::Devise is loadable. We use
30+
# `to_prepare` rather than a regular initializer because
31+
# `active_admin/devise` references `::Devise::SessionsController`
32+
# at file load, and that constant isn't autoloadable until
33+
# ActionDispatch has set up controller autoload paths — which
34+
# only happens after the initializer phase.
35+
initializer "activeadmin_oidc.register_controllers" do |app|
36+
app.config.to_prepare do
37+
require "active_admin/devise"
38+
unless ::ActiveAdmin::Devise.singleton_class < ControllersPatch
39+
::ActiveAdmin::Devise.singleton_class.prepend(ControllersPatch)
40+
end
41+
end
42+
end
43+
end
44+
end
45+
end
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveAdmin
4+
module Oidc
5+
# Builds the options hash the host app passes to
6+
# `Devise.setup { |c| c.omniauth :openid_connect, ... }`. Shape is
7+
# determined by the upstream `omniauth_openid_connect` gem.
8+
module OmniauthOptions
9+
module_function
10+
11+
def build(config = ActiveAdmin::Oidc.config)
12+
config.validate!
13+
14+
{
15+
name: Engine::PROVIDER_NAME,
16+
issuer: config.issuer,
17+
discovery: true,
18+
scope: config.scope.to_s.split(/\s+/).map(&:to_sym),
19+
response_type: :code,
20+
pkce: config.pkce,
21+
client_options: {
22+
identifier: config.issuer && config.client_id,
23+
secret: config.client_secret,
24+
port: nil,
25+
scheme: nil,
26+
host: nil
27+
}.compact
28+
}
29+
end
30+
end
31+
end
32+
end

spec/dummy/Rakefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "config/application"
4+
Rails.application.load_tasks

spec/dummy/app/admin/dashboard.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# frozen_string_literal: true
2+
3+
ActiveAdmin.register_page "Dashboard" do
4+
menu priority: 1, label: proc { I18n.t("active_admin.dashboard") }
5+
6+
content title: proc { I18n.t("active_admin.dashboard") } do
7+
para "Dummy dashboard."
8+
end
9+
end
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// no assets needed for the dummy app
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# frozen_string_literal: true
2+
3+
class ApplicationController < ActionController::Base
4+
end

0 commit comments

Comments
 (0)