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