Skip to main content

SSO & Plugin Architecture

This document describes the SSO implementation architecture, including the plugin protocol, supported providers, and the cross-instance SSO mechanism.

Architecture Overview​

SSO Protocol​

The SSO system is built on a Protocol pattern defined in app/services/auth/sso_protocol.py. Any SSO provider must implement the SSOProvider protocol:

@runtime_checkable
class SSOProvider(Protocol):
async def get_provider_config(self, org_slug: str) -> SSOProviderConfig | None: ...
async def initiate_login(self, org_slug: str, redirect_uri: str) -> str: ...
async def handle_callback(self, org_slug: str, code: str, state: str) -> SSOCallbackResult: ...
async def register_routes(self, app: Any) -> None: ...

Key Data Classes​

SSOProviderConfig — Public config returned to the frontend:

  • provider_type: "oidc" or "saml"
  • provider_name: Human-readable name (e.g., "Okta")
  • is_sso_required: Whether password login is disabled

SSOCallbackResult — Result from IdP callback processing:

  • email: Authenticated user's email
  • first_name / last_name: From IdP claims
  • claims: Raw claims dict
  • groups: IdP group names (for group-to-role mapping)

Supported Providers​

OIDC​

The OIDC flow follows the standard Authorization Code grant:

  1. Build authorization URL with response_type=code
  2. Redirect user to IdP
  3. Receive authorization code at callback
  4. Exchange code for tokens at the token endpoint
  5. Validate ID token and extract claims

SAML 2.0​

The SAML flow supports SP-initiated SSO:

  1. Build AuthnRequest and redirect to IdP SSO URL
  2. Receive SAML Response at the ACS endpoint (POST /sso/acs/:org)
  3. Validate assertion signature using IdP certificate
  4. Extract claims from assertion attributes

Additional SAML endpoints:

  • GET /sso/metadata/:org — SP metadata XML for IdP import

Cross-Instance SSO​

Cross-instance SSO allows a single IdP configuration to authenticate users across multiple Qarion instances.

How It Works​

  1. The SSOConfig model links to a target instance via instance_id
  2. During callback processing, _resolve_instance_slug() resolves the instance
  3. The JWT is created with instance_slug context
  4. User is provisioned in the target instance (via _provision_instance_user())
  5. Frontend redirects to the instance-specific path: /i/{instance_slug}/sso/callback

Instance User Provisioning​

When cross-instance SSO triggers, the service:

  • Checks if the user already has an InstanceUser record
  • Creates one if missing, with role=member and provisioned_via_sso=True
  • Does not overwrite existing instance membership

API Routes​

MethodPathAuthDescription
GET/sso/config/{org_slug}PublicSSO config for login page
GET/sso/login/{org_slug}PublicRedirect to IdP
GET/sso/callback/{org_slug}PublicOIDC callback handler
POST/sso/acs/{org_slug}PublicSAML ACS endpoint
GET/sso/metadata/{org_slug}PublicSAML SP metadata
GET/sso/logout/{org_slug}PublicSLO logout URL

Error Handling​

ExceptionHTTP StatusWhen
SSONotConfiguredError404No SSO config for the org
SSODomainNotAllowedError403Email domain not in allowed list
SSOProviderError502IdP communication failure
SSOServiceErrorvariesGeneral SSO service error

Extending SSO​

To add a new SSO provider:

  1. Implement the SSOProvider protocol
  2. Register it in the service layer
  3. Use register_routes() to mount any provider-specific endpoints
  4. The frontend dynamically adapts based on provider_type in the config response