Skip to content

SSO & SCIM Provisioning

Single sign-on (OIDC / SAML) and automated user provisioning via SCIM 2.0.

Cloud Feature

SSO and SCIM require the Cloud tier. API key authentication (kn_...) is available on all tiers.

SSO Integration

Kanoniv supports two SSO protocols for enterprise identity federation:

ProtocolUse Case
OIDCModern IdPs (Okta, Azure AD, Google Workspace, Auth0). Recommended for new setups.
SAML 2.0Legacy or compliance-mandated environments (ADFS, Ping Identity, OneLogin).

Both protocols follow the same pattern: the frontend initiates login via the API, the user authenticates at the IdP, and the callback endpoint exchanges credentials for a Kanoniv JWT. SSO-provisioned users are created with provisioning_source: "sso" and the viewer role.

How It Works

  1. Frontend calls POST /v1/auth/sso/initiate with the tenant slug and provider type
  2. API generates a CSRF state token (stored in Redis with 10-minute TTL) and returns a redirect URL to the IdP
  3. User authenticates at the IdP (Okta, Azure AD, etc.)
  4. IdP redirects back with an authorization code (OIDC) or SAML response
  5. Frontend sends the callback data to POST /v1/auth/sso/callback
  6. API validates the state token, exchanges the code/response for user identity, upserts the user, and issues a JWT
  7. Frontend uses the JWT for all subsequent API calls

Configuration

SSO configuration is managed through the settings API. Admin role required.

Create or Update an SSO Config

python
import httpx

client = httpx.Client(
    base_url="https://api.kanoniv.com",
    headers={"X-API-Key": "kn_live_..."}
)

# OIDC configuration (Okta example)
resp = client.post("/v1/settings/sso-config", json={
    "provider_type": "oidc",
    "display_name": "Okta SSO",
    "issuer_url": "https://api.kanoniv.com",
    "client_id": "0oa1b2c3d4e5f6g7h8",
    "client_secret_enc": "your-oidc-client-secret",
    "auth_url": "https://acme-corp.okta.com/oauth2/default/v1/authorize",
    "token_url": "https://acme-corp.okta.com/oauth2/default/v1/token",
    "jwks_url": "https://acme-corp.okta.com/oauth2/default/v1/keys",
    "is_active": True
})
sso_config = resp.json()
print(f"SSO config ID: {sso_config['id']}")
bash
# OIDC configuration (Okta example)
curl -X POST https://api.kanoniv.com/v1/settings/sso-config \
  -H "X-API-Key: kn_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "provider_type": "oidc",
    "display_name": "Okta SSO",
    "issuer_url": "https://api.kanoniv.com",
    "client_id": "0oa1b2c3d4e5f6g7h8",
    "client_secret_enc": "your-oidc-client-secret",
    "auth_url": "https://acme-corp.okta.com/oauth2/default/v1/authorize",
    "token_url": "https://acme-corp.okta.com/oauth2/default/v1/token",
    "jwks_url": "https://acme-corp.okta.com/oauth2/default/v1/keys",
    "is_active": true
  }'

SSO Config Fields

FieldRequiredDescription
provider_typeYes"oidc" or "saml"
display_nameYesHuman-readable name shown in the login UI
issuer_urlYesYour Kanoniv platform URL (used to build callback/ACS URLs)
client_idOIDCOAuth client ID from your IdP
client_secret_encOIDCOAuth client secret from your IdP
auth_urlYesIdP authorization endpoint (OIDC) or SSO URL (SAML)
token_urlOIDCIdP token endpoint
jwks_urlOIDCIdP JSON Web Key Set URL
saml_metadata_urlSAMLIdP SAML metadata URL (alternative to auth_url)
is_activeYesEnable or disable this SSO config

SAML Configuration

For SAML setups, provide your IdP with Kanoniv's SP metadata:

python
resp = client.get("/v1/auth/sso/metadata/acme-corp")
print(resp.text)  # SAML SP metadata XML
bash
# Download SP metadata XML for your IdP
curl https://api.kanoniv.com/v1/auth/sso/metadata/acme-corp

The metadata endpoint returns XML with:

  • Entity ID: urn:cannon:sp:{tenant_slug}
  • ACS URL: {issuer_url}/v1/auth/sso/callback
  • NameID format: emailAddress

Upload this XML to your IdP (Okta, Azure AD, ADFS) when configuring the SAML application.

List and Delete SSO Configs

python
# List all SSO configs for your tenant
configs = client.get("/v1/settings/sso-config").json()

# Delete a config by ID
client.delete(f"/v1/settings/sso-config/{configs[0]['id']}")
bash
# List all SSO configs
curl https://api.kanoniv.com/v1/settings/sso-config \
  -H "X-API-Key: kn_live_..."

# Delete a config
curl -X DELETE https://api.kanoniv.com/v1/settings/sso-config/{config_id} \
  -H "X-API-Key: kn_live_..."

SSO Login Flow

These endpoints are unauthenticated (they are the login mechanism). Rate limiting is applied.

Step 1: Initiate SSO

python
import httpx

client = httpx.Client(base_url="https://api.kanoniv.com")

resp = client.post("/v1/auth/sso/initiate", json={
    "tenant_slug": "acme-corp",
    "provider": "oidc"
})
data = resp.json()
# Redirect the user to data["redirect_url"]
print(f"Redirect to: {data['redirect_url']}")
bash
curl -X POST https://api.kanoniv.com/v1/auth/sso/initiate \
  -H "Content-Type: application/json" \
  -d '{"tenant_slug": "acme-corp", "provider": "oidc"}'

# Response:
# { "redirect_url": "https://acme-corp.okta.com/oauth2/default/v1/authorize?..." }

Step 2: User Authenticates at IdP

The user is redirected to the IdP login page. After authenticating, the IdP redirects back to your application with an authorization code (OIDC) or SAML response.

Step 3: Complete SSO Callback

python
# OIDC callback (after IdP redirect)
resp = client.post("/v1/auth/sso/callback", json={
    "provider": "oidc",
    "state": "state-from-redirect",
    "code": "authorization-code-from-idp"
})

auth = resp.json()
access_token = auth["token"]["access_token"]
refresh_token = auth["token"]["refresh_token"]
user = auth["user"]
print(f"Logged in as {user['email']} (role: {user['role']})")

# Use the JWT for subsequent API calls
authed = httpx.Client(
    base_url="https://api.kanoniv.com",
    headers={"Authorization": f"Bearer {access_token}"}
)
bash
# OIDC callback
curl -X POST https://api.kanoniv.com/v1/auth/sso/callback \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "oidc",
    "state": "state-from-redirect",
    "code": "authorization-code-from-idp"
  }'

# Response:
# {
#   "token": {
#     "access_token": "eyJ...",
#     "refresh_token": "rt_...",
#     "token_type": "Bearer"
#   },
#   "user": {
#     "id": "...",
#     "email": "[email protected]",
#     "name": "Jane Smith",
#     "role": "viewer"
#   }
# }

For SAML, send saml_response instead of code:

python
resp = client.post("/v1/auth/sso/callback", json={
    "provider": "saml",
    "state": "state-from-relay-state",
    "saml_response": "PHNhbWxwOlJlc3BvbnNlIC4uLj4..."  # base64-encoded
})

SCIM Provisioning

SCIM 2.0 (RFC 7644) enables your IdP to automatically create, update, and deactivate Kanoniv users. When someone joins or leaves your organization in Okta/Azure AD, the change propagates to Kanoniv automatically.

SCIM-provisioned users are created with:

  • provisioning_source: "scim"
  • role: "viewer" (default)
  • No local password (authentication is via SSO)

Key Behaviors

  • Create: IdP pushes a new user via POST /scim/v2/Users. The userName field maps to email, externalId is stored for correlation.
  • Update: IdP sends PUT (full replace) or PATCH (partial update) to modify user attributes.
  • Deactivate: IdP sends DELETE or patches active: false. Kanoniv performs a soft deactivate (sets is_verified = false) to preserve the user record for audit.
  • Conflict: If a user with the same email already exists in the tenant, POST returns 409 Conflict.

SCIM Setup

Step 1: Generate a SCIM Token

SCIM uses a separate bearer token (not your API key or JWT). The token hash is stored in tenants.settings.scim_token_hash.

Generate a token and store its SHA-256 hash in your tenant settings:

bash
# Generate a random SCIM token
SCIM_TOKEN=$(openssl rand -hex 32)
echo "Save this token for your IdP: $SCIM_TOKEN"

# Hash it and store in tenant settings
HASH=$(echo -n "$SCIM_TOKEN" | sha256sum | cut -d' ' -f1)
# Store the hash via your tenant settings (admin API or database)

Step 2: Configure Your IdP

Okta

  1. In Okta Admin, go to Applications > your SAML/OIDC app > Provisioning
  2. Enable SCIM connector
  3. Set the SCIM connector base URL: https://api.kanoniv.com/scim/v2
  4. Set authentication mode: HTTP Header
  5. Paste your SCIM token as the bearer token
  6. Enable: Create Users, Update User Attributes, Deactivate Users
  7. Map attributes:
    • userName -> user email
    • givenName / familyName -> user name
    • externalId -> Okta user ID

Azure AD (Entra ID)

  1. In Azure Portal, go to Enterprise Applications > your app > Provisioning
  2. Set provisioning mode to Automatic
  3. Set tenant URL: https://api.kanoniv.com/scim/v2
  4. Set secret token: your SCIM bearer token
  5. Test connection, then enable provisioning
  6. Map attributes under Attribute Mapping

Step 3: Test the Connection

python
import httpx

scim = httpx.Client(
    base_url="https://api.kanoniv.com/scim/v2",
    headers={"Authorization": "Bearer your-scim-token-here"}
)

# List users
resp = scim.get("/Users")
print(resp.json()["totalResults"])

# Create a test user
resp = scim.post("/Users", json={
    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
    "userName": "[email protected]",
    "name": {"givenName": "Test", "familyName": "User"},
    "externalId": "ext-12345",
    "active": True
})
user = resp.json()
print(f"Created: {user['id']}")
bash
# List SCIM users
curl https://api.kanoniv.com/scim/v2/Users \
  -H "Authorization: Bearer your-scim-token-here"

# Create a user
curl -X POST https://api.kanoniv.com/scim/v2/Users \
  -H "Authorization: Bearer your-scim-token-here" \
  -H "Content-Type: application/scim+json" \
  -d '{
    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
    "userName": "[email protected]",
    "name": {"givenName": "Test", "familyName": "User"},
    "externalId": "ext-12345",
    "active": true
  }'

SCIM Endpoints

All SCIM endpoints are under /scim/v2 and authenticate via Authorization: Bearer <scim_token>.

MethodPathDescription
GET/scim/v2/UsersList users. Supports filter, startIndex, count params.
GET/scim/v2/Users/:idGet a single user by ID.
POST/scim/v2/UsersCreate a new user. Returns 201 or 409 on conflict.
PUT/scim/v2/Users/:idFull replace of user attributes.
PATCH/scim/v2/Users/:idPartial update via SCIM PatchOp.
DELETE/scim/v2/Users/:idSoft deactivate user. Returns 204.

Filtering

The list endpoint supports the SCIM filter userName eq "email":

GET /scim/v2/Users?filter=userName eq "[email protected]"

Pagination

SCIM uses 1-based indexing:

GET /scim/v2/Users?startIndex=1&count=50

PATCH Operations

The PATCH endpoint accepts SCIM PatchOp with replace, add, and remove operations:

json
{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
  "Operations": [
    {"op": "replace", "path": "active", "value": false},
    {"op": "replace", "value": {"userName": "[email protected]"}},
    {"op": "replace", "path": "name.givenName", "value": "Janet"},
    {"op": "remove", "path": "externalId"}
  ]
}

Supported paths: active, userName, externalId, name.givenName, name.familyName. Unsupported paths are silently ignored; unsupported operations return 400.

End-to-End Cloud Workflow

This section shows how SSO, SCIM, and the Python SDK work together in a typical Cloud deployment. The IT team configures SSO and SCIM, users authenticate via their IdP, and analysts use the Python SDK to run reconciliation jobs.

1. IT Admin: Configure SSO + SCIM

python
import httpx

admin = httpx.Client(
    base_url="https://api.kanoniv.com",
    headers={"X-API-Key": "kn_live_admin_key"}
)

# Set up OIDC SSO
admin.post("/v1/settings/sso-config", json={
    "provider_type": "oidc",
    "display_name": "Okta SSO",
    "issuer_url": "https://api.kanoniv.com",
    "client_id": "0oa1b2c3d4e5f6g7h8",
    "client_secret_enc": "secret-from-okta",
    "auth_url": "https://acme-corp.okta.com/oauth2/default/v1/authorize",
    "token_url": "https://acme-corp.okta.com/oauth2/default/v1/token",
    "is_active": True
})

# SCIM is configured in the IdP pointing to:
#   Base URL: https://api.kanoniv.com/scim/v2
#   Auth: Bearer <scim-token>
# Users are auto-provisioned when assigned to the app in Okta.

2. IdP Auto-Provisions Team Members via SCIM

When the IT admin assigns users to the Kanoniv app in Okta, the IdP automatically calls the SCIM endpoints:

python
# This happens automatically via IdP -- shown here for illustration
scim = httpx.Client(
    base_url="https://api.kanoniv.com/scim/v2",
    headers={"Authorization": "Bearer scim-token"}
)

# Okta provisions three team members
for member in [
    {"userName": "[email protected]", "name": {"givenName": "Jane", "familyName": "Smith"}},
    {"userName": "[email protected]", "name": {"givenName": "Bob", "familyName": "Chen"}},
    {"userName": "[email protected]", "name": {"givenName": "Sara", "familyName": "Patel"}},
]:
    scim.post("/Users", json={
        "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
        **member,
        "active": True
    })

3. Analyst Logs In via SSO

python
import httpx

client = httpx.Client(base_url="https://api.kanoniv.com")

# Initiate SSO login
resp = client.post("/v1/auth/sso/initiate", json={
    "tenant_slug": "acme-corp",
    "provider": "oidc"
})
redirect_url = resp.json()["redirect_url"]
# User authenticates at Okta, gets redirected back with code + state

# Complete callback (in practice, the frontend handles this)
resp = client.post("/v1/auth/sso/callback", json={
    "provider": "oidc",
    "state": "state-param",
    "code": "auth-code-from-okta"
})
jwt = resp.json()["token"]["access_token"]

4. Analyst Runs Reconciliation with Python SDK

Once authenticated, the analyst uses the Kanoniv Python SDK with Source adapters to run identity resolution locally. The SDK operates independently of the SSO flow; it uses the local PyO3 reconciliation engine.

python
from kanoniv import Source, Spec, reconcile

# Load data from multiple sources using Source adapters
crm = Source.from_csv("crm", "crm_export.csv", primary_key="customer_id")

import pandas as pd
billing_df = pd.read_csv("stripe_customers.csv")
billing = Source.from_pandas("stripe", billing_df, primary_key="stripe_id")

warehouse = Source.from_warehouse(
    "postgres_customers",
    table="analytics.dim_customers",
    connection_string="postgresql://user:[email protected]/analytics",
    primary_key="customer_key"
)

dbt_orders = Source.from_dbt(
    "dbt_orders",
    model="ref('fct_orders')",
    manifest_path="target/manifest.json",
    connection_string="snowflake://user:[email protected]/PROD/ANALYTICS",
    primary_key="order_id"
)

# Load the reconciliation spec
spec = Spec.from_file("spec.yml")

# Run reconciliation with Cloud tier features
# (exclusions, scoring, compliance, freshness, hierarchy)
result = reconcile(
    sources=[crm, billing, warehouse, dbt_orders],
    spec=spec,
    tier="cloud"
)

print(f"Clusters: {result.cluster_count}")
print(f"Merge rate: {result.merge_rate:.2%}")
print(f"Sources reconciled: 4")

# Export the golden records
golden_df = result.to_pandas()
golden_df.to_csv("golden_records.csv", index=False)

5. Offboarding: SCIM Deprovisioning

When a team member leaves the organization and is removed from the Okta app, the IdP automatically deactivates them:

python
# IdP sends DELETE (automatic -- shown for illustration)
scim.delete(f"/Users/{bob_user_id}")  # Returns 204

# Bob's account is soft-deactivated (is_verified=false)
# His reconciliation history and audit trail are preserved

SSO vs API Keys

SSO and SCIM manage user identities, specifically who can log in to the dashboard and what role they have. The Python SDK authenticates using API keys (kn_...), which are separate from SSO sessions. An enterprise team typically uses SSO for dashboard access and API keys for programmatic SDK usage.

The identity and delegation layer for AI agents.