Module: StandardId::Oauth::AudienceProfileResolver

Defined in:
lib/standard_id/oauth/audience_profile_resolver.rb

Overview

Resolves the profile an account should be bound to for a given audience, based on ‘StandardId.config.oauth.audience_profile_types`.

The gem assumes the host app models profiles via ‘account.profiles` (the same shape assumed by `LifecycleHooks::DEFAULT_PROFILE_RESOLVER`). If the host app defines `StandardId.config.oauth.audience_profile_resolver`, that callable is used instead of the built-in lookup.

Examples:

StandardId::Oauth::AudienceProfileResolver.call(
  account: ,
  audience: "admin_kit"
)

Class Method Summary collapse

Class Method Details

.call(account:, audience:) ⇒ Object?

Returns the profile record for ‘audience`, or nil when no matching profile exists.

Callers should check ‘profile_types_for(audience).blank?` first when they need to distinguish “audience is unconfigured” from “account lacks a profile for a configured audience”.

Parameters:

  • account (Object)

    the authenticated account (must respond to #profiles)

  • audience (String, nil)

    the matched audience string

Returns:

  • (Object, nil)


28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/standard_id/oauth/audience_profile_resolver.rb', line 28

def call(account:, audience:)
  return nil if .nil? || audience.blank?

  types = profile_types_for(audience)
  return nil if types.empty?

  resolver = StandardId.config.oauth.audience_profile_resolver
  if resolver.respond_to?(:call)
    filtered = StandardId::Utils::CallableParameterFilter.filter(
      resolver,
      { account: , audience: audience, profile_types: types }
    )
    return resolver.call(**filtered)
  end

  default_lookup(, types)
end

.configured_for?(audience) ⇒ Boolean

True when audience_profile_types has a binding for ‘audience`.

Returns:

  • (Boolean)


58
59
60
# File 'lib/standard_id/oauth/audience_profile_resolver.rb', line 58

def configured_for?(audience)
  profile_types_for(audience).any?
end

.profile_types_for(audience) ⇒ Object

Returns the configured profile types (always as an Array<String>) for the given audience. Returns ‘[]` when no mapping is configured.



48
49
50
51
52
53
54
55
# File 'lib/standard_id/oauth/audience_profile_resolver.rb', line 48

def profile_types_for(audience)
  return [] if audience.blank?

  mapping = StandardId.config.oauth.audience_profile_types || {}
  return [] if mapping.empty?

  Array(mapping[audience.to_s] || mapping[audience.to_sym]).map(&:to_s).reject(&:blank?)
end

.resolve!(account:, audience:) ⇒ Object

Strict variant of ‘.call` for mint-time enforcement: returns the uniquely matching active profile, or raises a typed error so the token grant flow can fail closed.

Resolution rules (deterministic, no silent fallbacks):

- 0 matching active profiles → raises `NoBoundProfileError`
  (NB: an inactive-only match is still 0 active matches and
  fails closed — inactive profiles cannot mint tokens)
- exactly 1 matching active profile → returns it
- >1 matching active profile → raises `AmbiguousProfileError`

The legacy ‘.call` API preserves its “first active else first match” behavior, since it is wired into the decode-time concern and host apps may have grown to depend on its tolerance. Migrating that path to strict mode is a separate change.



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/standard_id/oauth/audience_profile_resolver.rb', line 80

def resolve!(account:, audience:)
  types = profile_types_for(audience)
  raise ArgumentError, "audience #{audience.inspect} has no profile binding" if types.empty?

  # Custom resolver path: trust the host app's result. It's expected
  # to enforce its own determinism — if it returns nil we still fail
  # closed; if it returns a profile we use it as-is.
  resolver = StandardId.config.oauth.audience_profile_resolver
  if resolver.respond_to?(:call)
    filtered = StandardId::Utils::CallableParameterFilter.filter(
      resolver,
      { account: , audience: audience, profile_types: types }
    )
    resolved = resolver.call(**filtered)
    return resolved if resolved
    raise StandardId::NoBoundProfileError.new(
      audience: audience,
      expected_profile_types: types
    )
  end

  strict_default_lookup(, audience, types)
end