Class: ActiveAdmin::Oidc::UserProvisioner

Inherits:
Object
  • Object
show all
Defined in:
lib/activeadmin/oidc/user_provisioner.rb

Overview

Finds-or-creates an AdminUser for an OIDC callback. Runs the host’s ‘on_login` hook (which owns all authorization decisions), then saves.

provisioner = UserProvisioner.new(config, claims: merged_claims, provider: "oidc")
admin_user  = provisioner.call  # raises ProvisioningError on denial

Strategy:

  1. Look up by (provider, uid). If found → update.

  2. Otherwise look up by the configured identity_attribute. If that row is already locked to a different (provider, uid) → refuse (account-takeover guard). Otherwise adopt it.

  3. Otherwise build a new record.

  4. Assign the identity attribute and oidc_raw_info.

  5. Call config.on_login(admin_user, claims). Falsy → deny. Truthy →save and return.

The claims hash is passed through untouched except that ‘access_token` and `refresh_token` (if present) are never persisted.

Constant Summary collapse

BLOCKED_RAW_INFO_KEYS =

Claim keys that must never land in oidc_raw_info.

%w[access_token refresh_token id_token].freeze

Instance Method Summary collapse

Constructor Details

#initialize(config, claims:, provider:) ⇒ UserProvisioner

Returns a new instance of UserProvisioner.



28
29
30
31
32
# File 'lib/activeadmin/oidc/user_provisioner.rb', line 28

def initialize(config, claims:, provider:)
  @config   = config
  @claims   = claims.transform_keys(&:to_s)
  @provider = provider
end

Instance Method Details

#callObject



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/activeadmin/oidc/user_provisioner.rb', line 34

def call
  validate_claims!

  admin_user = find_or_adopt_or_build

  # Retry path: a concurrent first sign-in inserted between our
  # initial miss-and-build and our failed save. Return the
  # winner's row verbatim — on_login already ran on our (now
  # discarded) in-memory build, and re-firing it would double
  # any host-side side effects (audit log, webhook, email).
  return admin_user if @retried

  assign_base_attributes(admin_user)

  allowed = (admin_user)
  raise ProvisioningError, denial_message unless allowed

  # Devise's `active_for_authentication?` guard runs in the
  # controller post-sign-in, but by then we've already saved
  # the record. Hostile attempts where on_login flips an
  # inactivity flag (e.g. enabled=false) would otherwise leave
  # provisional rows in the DB on every try. Refuse before
  # persisting. Raise the dedicated InactiveError so the
  # controller can surface the model's I18n inactive_message
  # instead of the generic denial flash.
  unless admin_user.active_for_authentication?
    raise InactiveError, admin_user.inactive_message
  end

  save!(admin_user)
  admin_user
rescue RetryProvisioning
  # Concurrent JIT provisioning: another thread inserted first.
  # Re-run once — find_or_adopt_or_build will now find the record.
  retry
end