Public API for OIDC / SAML identity federation and SCIM 2.0 directory
provisioning. Import root: rag_sso.
| Concern | Entry point |
|---|---|
| Federate a request (Auth SPI) | FederatedAuth |
| Verify an OIDC ID token | OidcProvider / OidcSettings |
| Verify a SAML assertion | SamlProvider / SamlSettings / signxml_verifier |
Map identity → Principal |
identity_to_principal / IdentityProvider |
| SCIM 2.0 provisioning | ScimService / parse_eq_filter |
| Low-level JWT | verify_jwt / encode_jwt_hs256 / decode_jwt_unverified |
Core types (rag_core.types): FederatedIdentity, SsoProtocol, ScimUser,
ScimGroup, ScimName, ScimEmail, ScimMember, ScimResourceMeta. SPI:
rag_core.spi.ScimStore + rag_core.spi.noop.NoopScimStore. Errors:
SsoError (401), ScimError (400), ScimNotFoundError (404), ScimConflictError
(409). Wire types (rag_core.gateway_types): ScimListResponse, ScimPatchOp,
ScimErrorBody, SsoStatusResponse.
from rag_sso import FederatedAuth, OidcProvider, OidcSettings
auth = FederatedAuth(
{"acme": OidcProvider(OidcSettings(
issuer="https://acme.okta.com", audience="agentcontextos",
algorithms=("RS256",), public_key=PEM, group_claim="groups",
))},
group_label_map={"engineering": "corpus-eng"},
)
principal = await auth.authenticate(id_token, TenantId("acme"))
# principal.acl_labels == {"corpus-eng", ...} — drives Step 6.3 push-downFederatedAuth is an Auth SPI backend — pass it as build_app(auth=…), or let
build_app_from_config build it from cfg.sso. A tenant with no provider rejects
bearer tokens (fail-closed); default_provider= sets a fallback.
The dependency-free default verifies HS256 (hmac_secret); RS256 / ES256 needs
the [oidc] extra (PyJWT) + a public_key. verify_jwt validates exp / nbf /
iss / aud and rejects any alg outside the allowlist.
from rag_sso import SamlProvider, SamlSettings, signxml_verifier
provider = SamlProvider(
SamlSettings(idp_entity_id="https://idp", audience="sp-entity"),
signature_verifier=signxml_verifier(idp_cert_pem), # [saml] extra
)
identity = provider.verify(base64_saml_response)With require_signature=True (default) and no verifier, SAML fails closed.
defusedxml (a core dep) makes parsing XXE/billion-laughs safe.
from rag_sso import ScimService
from rag_core.spi.noop import NoopScimStore
from rag_core.types import ScimUser
svc = ScimService(NoopScimStore())
user = await svc.create_user(ctx, ScimUser(user_name="alice@acme.test"))
await svc.patch_user(ctx, user.id, [{"op": "replace", "value": {"active": False}}]) # deprovision
page, total = await svc.list_users(ctx, scim_filter='userName eq "alice@acme.test"')All methods are ctx-first and tenant-scoped. Uniqueness violations raise
ScimConflictError; an unknown resource raises ScimNotFoundError; an unsupported
filter raises ScimError.
| Method / path | Purpose |
|---|---|
GET/POST /scim/v2/Users, GET/PUT/PATCH/DELETE /scim/v2/Users/{id} |
SCIM 2.0 User CRUD |
GET/POST /scim/v2/Groups, GET/PUT/PATCH/DELETE /scim/v2/Groups/{id} |
SCIM 2.0 Group CRUD |
GET /scim/v2/ServiceProviderConfig, /ResourceTypes |
SCIM discovery |
GET /v1/status/sso |
Calling tenant's SSO / SCIM posture |
SCIM auth is a per-tenant bearer token (Authorization: Bearer <token> +
X-Tenant-Id) configured in cfg.scim.tokens — independent of the JWT Auth
backend. Disabled SCIM → 404.
sso:
enabled: false # off by default (changes how principals are established)
group_label_map: {} # IdP group → ACL label (1:1 when absent)
scim:
enabled: false # off by default (a write surface)
base_path: /scim/v2
tokens: {} # {tenant_id: bearer-token}; ${ENV} supported
tenants:
- id: acme
sso: # per-tenant IdP
protocol: oidc | saml
oidc: { issuer, audience, algorithms, hmac_secret, public_key, group_claim, ... }
saml: { idp_entity_id, audience, certificate, group_attribute, require_signature, ... }hmac_secret / public_key / certificate / SCIM tokens all support
${ENV_VAR} interpolation so secrets stay out of the file.
ragctl sso list -f rag.yaml # inspect per-tenant IdP config
ragctl sso demo --tenant acme # in-process OIDC federation demo
ragctl scim --tenant acme # in-process SCIM provisioning demo| Event | When |
|---|---|
sso.authenticated / sso.auth_failed |
A federation attempt succeeds / fails |
scim.user_provisioned / scim.user_deprovisioned |
A user is created/replaced / deactivated or deleted |
scim.group_changed |
A group is created / replaced / deleted |
All are PII-free: the SSO event carries a hash of the subject; the SCIM event carries only the server-assigned resource id.
- Implement
Authfor non-IdP backends; implementScimStorefor a durable directory (conformance suite:tests/contract/test_scim_store.py). - Pass any
Callable[[bytes], bool]as a SAMLsignature_verifier. - See architecture/sso-scim.md and ADR-0040.