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 emailfirst_name/last_name: From IdP claimsclaims: Raw claims dictgroups: IdP group names (for group-to-role mapping)
Supported Providers
OIDC
The OIDC flow follows the standard Authorization Code grant:
- Build authorization URL with
response_type=code - Redirect user to IdP
- Receive authorization code at callback
- Exchange code for tokens at the token endpoint
- Validate ID token and extract claims
SAML 2.0
The SAML flow supports SP-initiated SSO:
- Build
AuthnRequestand redirect to IdP SSO URL - Receive SAML Response at the ACS endpoint (
POST /sso/acs/:org) - Validate assertion signature using IdP certificate
- 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
- The
SSOConfigmodel links to a target instance viainstance_id - During callback processing,
_resolve_instance_slug()resolves the instance - The JWT is created with
instance_slugcontext - User is provisioned in the target instance (via
_provision_instance_user()) - 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
InstanceUserrecord - Creates one if missing, with
role=memberandprovisioned_via_sso=True - Does not overwrite existing instance membership
API Routes
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /sso/config/{org_slug} | Public | SSO config for login page |
| GET | /sso/login/{org_slug} | Public | Redirect to IdP |
| GET | /sso/callback/{org_slug} | Public | OIDC callback handler |
| POST | /sso/acs/{org_slug} | Public | SAML ACS endpoint |
| GET | /sso/metadata/{org_slug} | Public | SAML SP metadata |
| GET | /sso/logout/{org_slug} | Public | SLO logout URL |
Error Handling
| Exception | HTTP Status | When |
|---|---|---|
SSONotConfiguredError | 404 | No SSO config for the org |
SSODomainNotAllowedError | 403 | Email domain not in allowed list |
SSOProviderError | 502 | IdP communication failure |
SSOServiceError | varies | General SSO service error |
Extending SSO
To add a new SSO provider:
- Implement the
SSOProviderprotocol - Register it in the service layer
- Use
register_routes()to mount any provider-specific endpoints - The frontend dynamically adapts based on
provider_typein the config response