Skip to content

SCIM 2.0 Integration Guide

Kordon supports SCIM (System for Cross-domain Identity Management) v2.0 for automated user and group provisioning from identity providers like:

  • Microsoft Azure Active Directory (Entra ID)
  • Okta
  • OneLogin
  • Google Workspace
  • Any SCIM 2.0 compliant identity provider

This enables enterprise customers to automatically sync their employee directory with Kordon, eliminating manual user management.

SCIM tokens are managed through the Kordon UI by administrators:

  1. Creating Tokens (Admin Only):

    • Navigate to Settings → Integrations
    • Scroll to SCIM Integrations section
    • Click Add SCIM Token
    • Enter a descriptive title (e.g., “Azure AD Production”, “Okta Staging”)
    • Token is generated and shown once - copy it immediately
    • Each token creates a dedicated bot user for audit trail
  2. Audit Trail:

    • Every SCIM change (user create/update/delete, group membership) is tracked
    • Bot user named: SCIM Integration: {token_title}
    • Changes appear in changelogs with [I] indicator
    • Full audit history retained in event store
  3. Token Revocation:

    • Delete token from UI → immediate invalidation
    • No server restart required
    • IdP will receive 401 Unauthorized on next sync
  4. Security:

    • Tokens stored securely in database (hashed recommended, currently plaintext)
    • Admin-only access to token management
    • Each integration can have separate token for isolation
    • Bot users have admin role (required for user/group management)

SCIM tokens are managed through the Kordon web interface (admin-only):

  1. Navigate to Settings → Integrations

    • Log in as an administrator
    • Go to Settings menu
    • Click “Integrations”
  2. Create a New SCIM Token

    • Scroll to “SCIM Integrations” section
    • Click “Add SCIM Token” button
    • Enter a descriptive title (e.g., “Azure AD Production”, “Okta Staging”)
    • Click “Create”
  3. Copy the Token

    • Token is displayed once in a modal
    • Copy it immediately and save securely
    • You will not be able to see it again
    • The modal also shows your SCIM Base URL
  4. Configure Your Identity Provider

    • Use the copied token as “Bearer Token” or “Secret Token”
    • Use the displayed SCIM Base URL (e.g., https://your-domain.com/scim/v2)
    • See identity provider setup guides below

Security Features:

  • Admin-only token management
  • Each token creates a dedicated bot user for audit trail
  • Bot user email: scim-{uuid}@kordon.app
  • All SCIM changes tracked in changelog with [I] indicator
  • IdP receives 401 Unauthorized on next sync after revocation

Token Management:

  • Create separate tokens for each environment (dev/staging/production)
  • Use descriptive titles to identify which IdP integration each token is for
  • Revoke tokens by deleting them from the UI
  • Monitor SCIM activity in user/group changelogs (look for [I] indicator)

Step 1: Create Enterprise Application

  1. Azure Portal → Enterprise Applications → New Application
  2. Create your own application → “Kordon SCIM Integration”
  3. Select “Non-gallery application”

Step 2: Configure Provisioning

  1. Select Provisioning → Get Started
  2. Provisioning Mode: Automatic
  3. Admin Credentials:
    • Tenant URL: https://your-kordon-domain.com/scim/v2
    • Secret Token: Your SCIM token from Kordon (Settings → Integrations)
  4. Test Connection → Save

Step 3: Attribute Mappings

Default mappings work out of the box:

  • userPrincipalNameuserName (email)
  • displayNamename.formatted
  • givenNamename.givenName
  • surnamename.familyName
  • mailNicknameexternalId (Azure AD object ID)
  • accountEnabledactive

Optional: Map Roles

To sync user roles from Azure AD to Kordon:

  1. Attribute Mappings → Show advanced options → Edit attribute list for Kordon
  2. Add custom attribute: roles
  3. Add mapping:
    • Azure AD Attribute: Choose an attribute or expression
    • Kordon Attribute: roles[primary eq "True"].value
    • Mapping type: Expression
    • Expression: Switch([extensionAttribute1], "admin", "manager", "auditor", "user")

Role Mapping Options:

  • Option 1: Custom attribute - Store role in user’s extensionAttribute1-15
  • Option 2: App Role assignment - Use Azure AD app roles (requires custom claim mapping)
  • Option 3: Group membership - Map specific Azure AD groups to groups.

Step 4: Assign Users

  1. Users and groups → Add user/group
  2. Select users/groups to provision
  3. Assign

Step 5: Start Provisioning

  1. Provisioning → Start provisioning
  2. Monitor: Provisioning → Provisioning logs

Sync Schedule: Every 40 minutes (Azure default)

Step 1: Create SCIM Integration

  1. Okta Admin Console → Applications → Create App Integration
  2. Sign-On Method: SAML 2.0 or Custom
  3. Configure SCIM under Provisioning tab

Step 2: SCIM Configuration

  1. Integration → Provisioning → Configure API Integration
  2. Enable API integration
  3. Base URL: https://your-kordon-domain.com/scim/v2
  4. API Token: Your SCIM token from Kordon (Settings → Integrations)
  5. Test API Credentials

Step 3: Provisioning Settings Enable:

  • Create Users
  • Update User Attributes
  • Deactivate Users

Optional:

  • Sync Password (not applicable for Kordon)

Step 4: Attribute Mappings Default Okta → SCIM mappings work:

  • emailuserName
  • firstName lastNamename.formatted
  • firstNamename.givenName
  • lastNamename.familyName
  • idexternalId

Step 5: Assign Users

  1. Assignments → Assign → Assign to People/Groups
  2. Select users to provision

Sync Schedule: Real-time for assignments, hourly for updates

Note: Google Workspace SCIM support is limited and may require custom app configuration.

Step 1: Create Custom SAML App

  1. Admin Console → Apps → Web and mobile apps → Add custom SAML app
  2. Configure SAML SSO

Step 2: Enable User Provisioning (if available)

  1. User provisioning → Setup
  2. API Endpoint: https://your-kordon-domain.com/scim/v2
  3. Authorization: Bearer token with your SCIM token from Kordon (Settings → Integrations)

Limitations:

  • Google Workspace SCIM support varies by subscription tier
  • May require Google Workspace Enterprise or Education editions
  • Consider using direct API integration as alternative
GET /scim/v2/ServiceProviderConfig
Authorization: Bearer {token}

Returns capabilities and configuration of the SCIM server.

GET /scim/v2/ResourceTypes
Authorization: Bearer {token}

Lists supported resource types (User, Group).

GET /scim/v2/Schemas
Authorization: Bearer {token}

Returns SCIM schemas for all resources.

GET /scim/v2/Users?startIndex=1&count=100
Authorization: Bearer {token}

Query Parameters:

  • startIndex - Pagination offset (1-based)
  • count - Results per page (default: 100)
  • filter - SCIM filter expression

Filter Examples:

?filter=userName eq "user@example.com"
?filter=externalId eq "azure-ad-object-id"
?filter=active eq true
GET /scim/v2/Users/{id}
Authorization: Bearer {token}

Returns user by Kordon’s internal UUID.

POST /scim/v2/Users
Authorization: Bearer {token}
Content-Type: application/scim+json
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"externalId": "azure-ad-12345",
"userName": "john.doe@company.com",
"name": {
"formatted": "John Doe",
"givenName": "John",
"familyName": "Doe"
},
"emails": [{
"value": "john.doe@company.com",
"type": "work",
"primary": true
}],
"active": true
}

Auto-created Resources:

  • Personal group (1-member group for ownership)
  • User record with default role: "user"
PUT /scim/v2/Users/{id}
Authorization: Bearer {token}
Content-Type: application/scim+json
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "john.doe@company.com",
"name": {
"formatted": "John A. Doe",
"givenName": "John",
"familyName": "Doe"
},
"active": true
}
PATCH /scim/v2/Users/{id}
Authorization: Bearer {token}
Content-Type: application/scim+json
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [{
"op": "replace",
"path": "active",
"value": false
}]
}

Common PATCH operations:

  • Deactivate: {"op": "replace", "path": "active", "value": false}
  • Activate: {"op": "replace", "path": "active", "value": true}
  • Update name: {"op": "replace", "path": "name.formatted", "value": "New Name"}
DELETE /scim/v2/Users/{id}
Authorization: Bearer {token}

Note: This performs a soft delete (sets deactivated_at), not hard delete.

Groups in SCIM map to UserGroup (with kind='regular') in Kordon. Personal groups are excluded from SCIM operations.

GET /scim/v2/Groups?startIndex=1&count=100
Authorization: Bearer {token}

Query Parameters:

  • startIndex - Pagination offset (1-based)
  • count - Results per page (default: 100)
  • filter - SCIM filter expression

Filter Examples:

?filter=displayName eq "Engineers"
?filter=displayName co "Marketing"

Response:

{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults": 10,
"startIndex": 1,
"itemsPerPage": 10,
"Resources": [
{
"id": "341701d0-86f2-4a58-af1c-25bc43705394",
"externalId": null,
"displayName": "Engineers",
"members": [
{
"value": "7f341270-5408-410e-9d88-408911a4a8ff",
"display": "Juhan Juku"
},
{
"value": "93f178e7-1c05-4997-97d0-0c22001ce2dc",
"display": "Juuri Puuri"
}
],
"meta": {
"resourceType": "Group",
"created": "2025-09-04T12:04:27Z",
"lastModified": "2025-10-16T14:02:35Z",
"location": "http://localhost:4000/scim/v2/Groups/341701d0-86f2-4a58-af1c-25bc43705394"
},
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"]
}
]
}
GET /scim/v2/Groups/{id}
Authorization: Bearer {token}

Returns a single group with members list.

POST /scim/v2/Groups
Authorization: Bearer {token}
Content-Type: application/scim+json
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"displayName": "New Team",
"externalId": "azure-ad-object-id-123",
"members": [
{
"value": "user-uuid-1",
"display": "John Doe"
},
{
"value": "user-uuid-2",
"display": "Jane Smith"
}
]
}

Creates a new UserGroup with kind='regular' and adds specified members.

PUT /scim/v2/Groups/{id}
Authorization: Bearer {token}
Content-Type: application/scim+json
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"displayName": "Updated Team Name",
"externalId": "azure-ad-object-id-123",
"members": [
{
"value": "user-uuid-1",
"display": "John Doe"
}
]
}

Replaces all group attributes including members. Missing members are removed.

PATCH /scim/v2/Groups/{id}
Authorization: Bearer {token}
Content-Type: application/scim+json
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "add",
"path": "members",
"value": [
{
"value": "user-uuid-3",
"display": "New Member"
}
]
},
{
"op": "remove",
"path": "members[value eq \"user-uuid-2\"]"
},
{
"op": "replace",
"path": "displayName",
"value": "Renamed Team"
}
]
}

Partial updates using SCIM PATCH operations:

  • add - Add members to group
  • remove - Remove members from group
  • replace - Update group name or other attributes
DELETE /scim/v2/Groups/{id}
Authorization: Bearer {token}

Note: This performs a soft delete (sets deleted_at via paranoia gem).

Then create app/controllers/scim/v2/groups_controller.rb similar to users controller.

1. Test Authentication:

Terminal window
export SCIM_TOKEN="your-bearer-token"
export KORDON_URL="https://your-kordon-domain.com"
curl -X GET \
"$KORDON_URL/scim/v2/ServiceProviderConfig" \
-H "Authorization: Bearer $SCIM_TOKEN" \
-H "Content-Type: application/scim+json"

2. List Users:

Terminal window
curl -X GET \
"$KORDON_URL/scim/v2/Users" \
-H "Authorization: Bearer $SCIM_TOKEN" \
-H "Content-Type: application/scim+json"

3. Create Test User:

Terminal window
curl -X POST \
"$KORDON_URL/scim/v2/Users" \
-H "Authorization: Bearer $SCIM_TOKEN" \
-H "Content-Type: application/scim+json" \
-d '{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "test.user@example.com",
"name": {
"formatted": "Test User",
"givenName": "Test",
"familyName": "User"
},
"emails": [{
"value": "test.user@example.com",
"type": "work",
"primary": true
}],
"active": true,
"externalId": "test-external-id-123"
}'

4. Filter Users:

Terminal window
# By email
curl -X GET \
"$KORDON_URL/scim/v2/Users?filter=userName%20eq%20%22test.user@example.com%22" \
-H "Authorization: Bearer $SCIM_TOKEN"
# By external ID
curl -X GET \
"$KORDON_URL/scim/v2/Users?filter=externalId%20eq%20%22test-external-id-123%22" \
-H "Authorization: Bearer $SCIM_TOKEN"

Import this collection for comprehensive SCIM testing:

{
"info": {
"name": "Kordon SCIM v2",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"variable": [
{
"key": "base_url",
"value": "https://your-kordon-domain.com"
},
{
"key": "bearer_token",
"value": "your-scim-bearer-token"
}
],
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{bearer_token}}"
}
]
}
}

Error: 401 Unauthorized

Causes:

  • Missing Authorization header
  • Invalid bearer token
  • SCIM token deleted or revoked in Kordon UI
  • Token not created yet

Solution:

  1. Verify token exists:

    • Log into Kordon as admin
    • Go to Settings → Integrations
    • Check if SCIM token is listed in “SCIM Integrations” section
  2. Create new token if needed:

    • Click “Add SCIM Token”
    • Enter descriptive title
    • Copy the generated token (shown once)
    • Update your identity provider configuration
  3. No restart required:

    • Token changes are immediate
    • IdP will authenticate with new token on next sync

Error: 422 Unprocessable Entity or validation errors

Causes:

  • Email already exists (must be unique)
  • Missing required fields (userName, name)
  • Invalid email format

Solution:

  • Check user doesn’t already exist
  • Ensure all required SCIM attributes provided
  • Validate email format

Error: Duplicate external_id violation

Cause: IdP trying to create user with existing external_id

Solution:

  1. Check if user was already provisioned
  2. IdP may need to query first: GET /scim/v2/Users?filter=externalId eq "..."
  3. If exists, use PUT/PATCH to update instead of POST