Skip to content

Latest commit

 

History

History
141 lines (110 loc) · 5.43 KB

File metadata and controls

141 lines (110 loc) · 5.43 KB

SSO / SCIM reference (rag-sso, Step 6.8)

Public API for OIDC / SAML identity federation and SCIM 2.0 directory provisioning. Import root: rag_sso.

Overview

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.

Usage

Federate a request

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-down

FederatedAuth 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.

Verify a SAML assertion

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.

SCIM provisioning

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.

HTTP surfaces

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.

Configuration

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

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

Events

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.

Extension points

  • Implement Auth for non-IdP backends; implement ScimStore for a durable directory (conformance suite: tests/contract/test_scim_store.py).
  • Pass any Callable[[bytes], bool] as a SAML signature_verifier.
  • See architecture/sso-scim.md and ADR-0040.