Module: OpenLoam::Sso
- Defined in:
- lib/open_loam/sso.rb,
lib/open_loam/sso/claims.rb,
lib/open_loam/sso/http_client.rb,
lib/open_loam/sso/fake_provider.rb,
lib/open_loam/sso/oidc_provider.rb
Overview
Single sign-on. SHIPPED: OIDC Authorization Code flow end-to-end — home-realm discovery by email domain, just-in-time user provisioning, IdP group -> role mapping, and account linking to an existing User. SEAMS (documented, not built): SAML (another protocol behind the same interface, raises NotImplementedError until implemented) and SCIM 2.0 provisioning (see docs/_foundation/overview.md). See OpenLoam::SsoProvider for the per-tenant config.
A protocol provider implements two methods:
authorization_url(state:, login_hint: nil) -> the IdP URL to redirect to
exchange(code:) -> OpenLoam::Sso::Claims
The provider is built through a swappable builder so tests and the demo
inject a FakeProvider and NOTHING touches the network. OpenLoam::Sso::OidcProvider
is the real one; it is never constructed in the test suite.
Defined Under Namespace
Modules: HttpClient Classes: Claims, DomainMismatchError, Error, FakeProvider, OidcProvider, UnverifiedDomainError, UnverifiedEmailError
Class Attribute Summary collapse
Class Method Summary collapse
-
.build(record, redirect_uri:) ⇒ Object
Wrap a OpenLoam::SsoProvider record in its protocol provider.
- .default_builder(record, redirect_uri) ⇒ Object
-
.provider_for(email:) ⇒ Object
Home-realm discovery.
-
.provision(provider, claims) ⇒ Object
Resolve verified IdP claims to a signed-in User, in the provider's tenant.
-
.role_for(provider, claims) ⇒ Object
IdP group -> role; first configured group that the user carries wins, otherwise the provider's default role.
Class Attribute Details
.builder ⇒ Object
43 44 45 |
# File 'lib/open_loam/sso.rb', line 43 def builder @builder ||= method(:default_builder) end |
Class Method Details
.build(record, redirect_uri:) ⇒ Object
Wrap a OpenLoam::SsoProvider record in its protocol provider.
48 49 50 |
# File 'lib/open_loam/sso.rb', line 48 def build(record, redirect_uri:) builder.call(record, redirect_uri) end |
.default_builder(record, redirect_uri) ⇒ Object
52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/open_loam/sso.rb', line 52 def default_builder(record, redirect_uri) case record.protocol when "oidc" OidcProvider.new(record, redirect_uri: redirect_uri) when "saml" raise NotImplementedError, "SAML SSO is a documented seam — implement OpenLoam::Sso::SamlProvider behind this interface." else raise Error, "Unsupported SSO protocol: #{record.protocol.inspect}" end end |
.provider_for(email:) ⇒ Object
Home-realm discovery. THE blessed cross-tenant lookup for SSO (like
OpenLoam::Membership.tenants_for): "which tenant's IdP owns this email
domain?" is asked at the sign-in page, before any tenant is chosen, so it
reaches across tenants via unscoped — something host code must never do.
Verified providers only: the domain is manager-typed, so an unverified claim would capture every sign-in on a domain the tenant does not own.
71 72 73 74 75 76 |
# File 'lib/open_loam/sso.rb', line 71 def provider_for(email:) domain = email.to_s.split("@").last.to_s.strip.downcase return nil if domain.blank? OpenLoam::SsoProvider.unscoped.domain_verified.where(domain: domain, active: true).first end |
.provision(provider, claims) ⇒ Object
Resolve verified IdP claims to a signed-in User, in the provider's tenant. MUST be called inside OpenLoam.as_tenant(provider.tenant). Order matters: match the durable (provider, sub) identity first, then an existing User by verified email (link), else just-in-time create — always with a tenant membership at the mapped role.
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/open_loam/sso.rb', line 83 def provision(provider, claims) raise UnverifiedEmailError, "the identity provider did not verify #{claims.email.inspect}" unless claims.email_verified? # The provider only vouches for its own domain. Check this BEFORE any # lookup, link, or create — an email on another domain is refused # outright (no session, nothing touched). email_domain = claims.email.to_s.split("@").last.to_s.downcase unless email_domain.present? && email_domain == provider.domain.to_s.downcase raise DomainMismatchError, "verified email domain #{email_domain.inspect} is not this provider's domain #{provider.domain.inspect}" end # Resolve the user: the durable (provider, sub) identity first, then an # existing User by verified email (link), else just-in-time create. identity = OpenLoam::SsoIdentity.find_by(sso_provider_id: provider.id, sub: claims.sub) user = identity&.user || link_or_create_user(provider, claims) # Re-map claims -> role on EVERY login (including a returning identity), # so an IdP role change takes effect. ensure_membership(provider, user, claims) OpenLoam::SsoIdentity.create!(user: user, sso_provider: provider, sub: claims.sub) unless identity user end |
.role_for(provider, claims) ⇒ Object
IdP group -> role; first configured group that the user carries wins, otherwise the provider's default role.
109 110 111 112 113 |
# File 'lib/open_loam/sso.rb', line 109 def role_for(provider, claims) map = provider.group_roles match = Array(claims.groups).map(&:to_s).find { |group| map.key?(group) } match ? map[match] : provider.jit_role end |