From 50868ac8ea75689b2e4930e9346361679ad58a09 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Mon, 12 Jan 2026 12:47:52 +0000 Subject: [PATCH] feat: Add SCIM Schemas and ResourceTypes endpoints for project and status page --- App/FeatureSet/Identity/API/SCIM.ts | 72 +++- App/FeatureSet/Identity/API/StatusPageSCIM.ts | 68 +++- App/FeatureSet/Identity/Utils/SCIMUtils.ts | 363 +++++++++++++++++- Docs/Content/identity/scim.md | 347 +++++++++++++++-- 4 files changed, 805 insertions(+), 45 deletions(-) diff --git a/App/FeatureSet/Identity/API/SCIM.ts b/App/FeatureSet/Identity/API/SCIM.ts index 64f2b237f1..5551cb7f53 100644 --- a/App/FeatureSet/Identity/API/SCIM.ts +++ b/App/FeatureSet/Identity/API/SCIM.ts @@ -31,6 +31,8 @@ import { generateServiceProviderConfig, generateUsersListResponse, parseSCIMQueryParams, + generateSchemasResponse, + generateResourceTypesResponse, } from "../Utils/SCIMUtils"; import { AppApiClientUrl, @@ -212,6 +214,64 @@ router.get( }, ); +// SCIM Schemas endpoint - GET /scim/v2/Schemas +router.get( + "/scim/v2/:projectScimId/Schemas", + SCIMMiddleware.isAuthorizedSCIMRequest, + async ( + req: ExpressRequest, + res: ExpressResponse, + next: NextFunction, + ): Promise => { + try { + logger.debug( + `Project SCIM Schemas - scimId: ${req.params["projectScimId"]!}`, + ); + + const schemasResponse: JSONObject = generateSchemasResponse( + req, + req.params["projectScimId"]!, + "project", + ); + + logger.debug("Project SCIM Schemas response prepared successfully"); + return Response.sendJsonObjectResponse(req, res, schemasResponse); + } catch (err) { + logger.error(err); + return next(err); + } + }, +); + +// SCIM ResourceTypes endpoint - GET /scim/v2/ResourceTypes +router.get( + "/scim/v2/:projectScimId/ResourceTypes", + SCIMMiddleware.isAuthorizedSCIMRequest, + async ( + req: ExpressRequest, + res: ExpressResponse, + next: NextFunction, + ): Promise => { + try { + logger.debug( + `Project SCIM ResourceTypes - scimId: ${req.params["projectScimId"]!}`, + ); + + const resourceTypesResponse: JSONObject = generateResourceTypesResponse( + req, + req.params["projectScimId"]!, + "project", + ); + + logger.debug("Project SCIM ResourceTypes response prepared successfully"); + return Response.sendJsonObjectResponse(req, res, resourceTypesResponse); + } catch (err) { + logger.error(err); + return next(err); + } + }, +); + // Basic Users endpoint - GET /scim/v2/Users router.get( "/scim/v2/:projectScimId/Users", @@ -378,10 +438,10 @@ router.get( }, ); - // now paginate the results + // now paginate the results (startIndex is 1-based in SCIM) const paginatedUsers: Array = users.slice( - (startIndex - 1) * count, - startIndex * count, + startIndex - 1, + startIndex - 1 + count, ); logger.debug(`SCIM Users response prepared with ${users.length} users`); @@ -716,10 +776,10 @@ router.get( const groups: Array = await Promise.all(groupsPromises); - // Paginate results + // Paginate results (startIndex is 1-based in SCIM) const paginatedGroups: Array = groups.slice( - (startIndex - 1) * count, - startIndex * count, + startIndex - 1, + startIndex - 1 + count, ); logger.debug( diff --git a/App/FeatureSet/Identity/API/StatusPageSCIM.ts b/App/FeatureSet/Identity/API/StatusPageSCIM.ts index e533ddbfc2..dcf5fd8bad 100644 --- a/App/FeatureSet/Identity/API/StatusPageSCIM.ts +++ b/App/FeatureSet/Identity/API/StatusPageSCIM.ts @@ -20,6 +20,8 @@ import LIMIT_MAX, { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax"; import { formatUserForSCIM, generateServiceProviderConfig, + generateSchemasResponse, + generateResourceTypesResponse, } from "../Utils/SCIMUtils"; import Text from "Common/Types/Text"; import HashedString from "Common/Types/HashedString"; @@ -54,6 +56,66 @@ router.get( }, ); +// SCIM Schemas endpoint - GET /status-page-scim/v2/Schemas +router.get( + "/status-page-scim/v2/:statusPageScimId/Schemas", + SCIMMiddleware.isAuthorizedSCIMRequest, + async ( + req: ExpressRequest, + res: ExpressResponse, + next: NextFunction, + ): Promise => { + try { + logger.debug( + `Status Page SCIM Schemas - scimId: ${req.params["statusPageScimId"]!}`, + ); + + const schemasResponse: JSONObject = generateSchemasResponse( + req, + req.params["statusPageScimId"]!, + "status-page", + ); + + logger.debug("Status Page SCIM Schemas response prepared successfully"); + return Response.sendJsonObjectResponse(req, res, schemasResponse); + } catch (err) { + logger.error(err); + return next(err); + } + }, +); + +// SCIM ResourceTypes endpoint - GET /status-page-scim/v2/ResourceTypes +router.get( + "/status-page-scim/v2/:statusPageScimId/ResourceTypes", + SCIMMiddleware.isAuthorizedSCIMRequest, + async ( + req: ExpressRequest, + res: ExpressResponse, + next: NextFunction, + ): Promise => { + try { + logger.debug( + `Status Page SCIM ResourceTypes - scimId: ${req.params["statusPageScimId"]!}`, + ); + + const resourceTypesResponse: JSONObject = generateResourceTypesResponse( + req, + req.params["statusPageScimId"]!, + "status-page", + ); + + logger.debug( + "Status Page SCIM ResourceTypes response prepared successfully", + ); + return Response.sendJsonObjectResponse(req, res, resourceTypesResponse); + } catch (err) { + logger.error(err); + return next(err); + } + }, +); + // Status Page Users endpoint - GET /status-page-scim/v2/Users router.get( "/status-page-scim/v2/:statusPageScimId/Users", @@ -154,10 +216,10 @@ router.get( }, ); - // Paginate the results + // Paginate the results (startIndex is 1-based in SCIM) const paginatedUsers: Array = users.slice( - (startIndex - 1) * count, - startIndex * count, + startIndex - 1, + startIndex - 1 + count, ); logger.debug( diff --git a/App/FeatureSet/Identity/Utils/SCIMUtils.ts b/App/FeatureSet/Identity/Utils/SCIMUtils.ts index b0a7aae594..41245d01b7 100644 --- a/App/FeatureSet/Identity/Utils/SCIMUtils.ts +++ b/App/FeatureSet/Identity/Utils/SCIMUtils.ts @@ -4,6 +4,23 @@ import { JSONObject } from "Common/Types/JSON"; import Email from "Common/Types/Email"; import Name from "Common/Types/Name"; import ObjectID from "Common/Types/ObjectID"; +import Exception from "Common/Types/Exception/Exception"; + +/** + * SCIM Error types as defined in RFC 7644 + */ +export enum SCIMErrorType { + InvalidFilter = "invalidFilter", + TooMany = "tooMany", + Uniqueness = "uniqueness", + Mutability = "mutability", + InvalidSyntax = "invalidSyntax", + InvalidPath = "invalidPath", + NoTarget = "noTarget", + InvalidValue = "invalidValue", + InvalidVers = "invalidVers", + Sensitive = "sensitive", +} /** * Shared SCIM utility functions for both Project SCIM and Status Page SCIM @@ -172,9 +189,9 @@ export const generateServiceProviderConfig: ( supported: true, }, bulk: { - supported: true, - maxOperations: 1000, - maxPayloadSize: 1048576, + supported: false, + maxOperations: 0, + maxPayloadSize: 0, }, filter: { supported: true, @@ -263,3 +280,343 @@ export const parseSCIMQueryParams: (req: ExpressRequest) => { return { startIndex, count }; }; + +/** + * Generate SCIM-compliant error response as per RFC 7644 + */ +export const generateSCIMErrorResponse: ( + status: number, + detail: string, + scimType?: SCIMErrorType, +) => JSONObject = ( + status: number, + detail: string, + scimType?: SCIMErrorType, +): JSONObject => { + const errorResponse: JSONObject = { + schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"], + status: status.toString(), + detail: detail, + }; + + if (scimType) { + errorResponse["scimType"] = scimType; + } + + return errorResponse; +}; + +/** + * Generate SCIM Schemas endpoint response + */ +export const generateSchemasResponse: ( + req: ExpressRequest, + scimId: string, + scimType: "project" | "status-page", +) => JSONObject = ( + req: ExpressRequest, + scimId: string, + scimType: "project" | "status-page", +): JSONObject => { + const baseUrl: string = `${req.protocol}://${req.get("host")}`; + const endpointPath: string = + scimType === "project" + ? `/scim/v2/${scimId}` + : `/status-page-scim/v2/${scimId}`; + + const schemas: JSONObject[] = [ + { + id: "urn:ietf:params:scim:schemas:core:2.0:User", + name: "User", + description: "User Schema", + attributes: [ + { + name: "userName", + type: "string", + multiValued: false, + description: "Unique identifier for the User, typically email address", + required: true, + caseExact: false, + mutability: "readWrite", + returned: "default", + uniqueness: "server", + }, + { + name: "name", + type: "complex", + multiValued: false, + description: "The components of the user's name", + required: false, + subAttributes: [ + { + name: "formatted", + type: "string", + multiValued: false, + description: "The full name", + required: false, + mutability: "readWrite", + returned: "default", + }, + { + name: "familyName", + type: "string", + multiValued: false, + description: "The family name or last name", + required: false, + mutability: "readWrite", + returned: "default", + }, + { + name: "givenName", + type: "string", + multiValued: false, + description: "The given name or first name", + required: false, + mutability: "readWrite", + returned: "default", + }, + ], + mutability: "readWrite", + returned: "default", + }, + { + name: "displayName", + type: "string", + multiValued: false, + description: "The name of the User suitable for display", + required: false, + mutability: "readWrite", + returned: "default", + }, + { + name: "emails", + type: "complex", + multiValued: true, + description: "Email addresses for the user", + required: false, + subAttributes: [ + { + name: "value", + type: "string", + multiValued: false, + description: "Email address value", + required: false, + mutability: "readWrite", + returned: "default", + }, + { + name: "type", + type: "string", + multiValued: false, + description: "Type of email (work, home, other)", + required: false, + canonicalValues: ["work", "home", "other"], + mutability: "readWrite", + returned: "default", + }, + { + name: "primary", + type: "boolean", + multiValued: false, + description: "Indicates if this is the primary email", + required: false, + mutability: "readWrite", + returned: "default", + }, + ], + mutability: "readWrite", + returned: "default", + }, + { + name: "active", + type: "boolean", + multiValued: false, + description: "Indicates whether the user is active", + required: false, + mutability: "readWrite", + returned: "default", + }, + ], + meta: { + resourceType: "Schema", + location: `${baseUrl}${endpointPath}/Schemas/urn:ietf:params:scim:schemas:core:2.0:User`, + }, + }, + ]; + + // Add Group schema only for project SCIM + if (scimType === "project") { + schemas.push({ + id: "urn:ietf:params:scim:schemas:core:2.0:Group", + name: "Group", + description: "Group Schema (Teams in OneUptime)", + attributes: [ + { + name: "displayName", + type: "string", + multiValued: false, + description: "Human-readable name for the Group/Team", + required: true, + mutability: "readWrite", + returned: "default", + uniqueness: "server", + }, + { + name: "members", + type: "complex", + multiValued: true, + description: "A list of members of the Group", + required: false, + subAttributes: [ + { + name: "value", + type: "string", + multiValued: false, + description: "Identifier of the member", + required: false, + mutability: "immutable", + returned: "default", + }, + { + name: "$ref", + type: "reference", + referenceTypes: ["User"], + multiValued: false, + description: "URI of the member resource", + required: false, + mutability: "immutable", + returned: "default", + }, + { + name: "display", + type: "string", + multiValued: false, + description: "Display name of the member", + required: false, + mutability: "immutable", + returned: "default", + }, + ], + mutability: "readWrite", + returned: "default", + }, + ], + meta: { + resourceType: "Schema", + location: `${baseUrl}${endpointPath}/Schemas/urn:ietf:params:scim:schemas:core:2.0:Group`, + }, + }); + } + + return { + schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], + totalResults: schemas.length, + itemsPerPage: schemas.length, + startIndex: 1, + Resources: schemas, + }; +}; + +/** + * Generate SCIM ResourceTypes endpoint response + */ +export const generateResourceTypesResponse: ( + req: ExpressRequest, + scimId: string, + scimType: "project" | "status-page", +) => JSONObject = ( + req: ExpressRequest, + scimId: string, + scimType: "project" | "status-page", +): JSONObject => { + const baseUrl: string = `${req.protocol}://${req.get("host")}`; + const endpointPath: string = + scimType === "project" + ? `/scim/v2/${scimId}` + : `/status-page-scim/v2/${scimId}`; + + const resourceTypes: JSONObject[] = [ + { + schemas: ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"], + id: "User", + name: "User", + endpoint: "/Users", + description: "User Account", + schema: "urn:ietf:params:scim:schemas:core:2.0:User", + schemaExtensions: [], + meta: { + resourceType: "ResourceType", + location: `${baseUrl}${endpointPath}/ResourceTypes/User`, + }, + }, + ]; + + // Add Group resource type only for project SCIM + if (scimType === "project") { + resourceTypes.push({ + schemas: ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"], + id: "Group", + name: "Group", + endpoint: "/Groups", + description: "Group (Team in OneUptime)", + schema: "urn:ietf:params:scim:schemas:core:2.0:Group", + schemaExtensions: [], + meta: { + resourceType: "ResourceType", + location: `${baseUrl}${endpointPath}/ResourceTypes/Group`, + }, + }); + } + + return { + schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], + totalResults: resourceTypes.length, + itemsPerPage: resourceTypes.length, + startIndex: 1, + Resources: resourceTypes, + }; +}; + +/** + * Map HTTP status codes to SCIM error types + */ +export const getScimErrorTypeFromException: ( + err: Exception, +) => SCIMErrorType | undefined = (err: Exception): SCIMErrorType | undefined => { + const errorName: string = err.constructor.name; + + switch (errorName) { + case "BadRequestException": + return SCIMErrorType.InvalidValue; + case "NotFoundException": + return SCIMErrorType.NoTarget; + case "NotAuthorizedException": + return undefined; // No specific SCIM type for auth errors + default: + return undefined; + } +}; + +/** + * Get HTTP status code from exception + */ +export const getHttpStatusFromException: (err: Exception) => number = ( + err: Exception, +): number => { + const errorName: string = err.constructor.name; + + switch (errorName) { + case "BadRequestException": + return 400; + case "NotAuthorizedException": + return 401; + case "PaymentRequiredException": + return 402; + case "NotFoundException": + return 404; + case "NotImplementedException": + return 501; + default: + return 500; + } +}; diff --git a/Docs/Content/identity/scim.md b/Docs/Content/identity/scim.md index 52657f364c..3e17571ba3 100644 --- a/Docs/Content/identity/scim.md +++ b/Docs/Content/identity/scim.md @@ -35,11 +35,18 @@ Project SCIM allows identity providers to manage team members within OneUptime p ### Project SCIM Endpoints - **Service Provider Config**: `GET /scim/v2/{scimId}/ServiceProviderConfig` +- **Schemas**: `GET /scim/v2/{scimId}/Schemas` +- **Resource Types**: `GET /scim/v2/{scimId}/ResourceTypes` - **List Users**: `GET /scim/v2/{scimId}/Users` - **Get User**: `GET /scim/v2/{scimId}/Users/{userId}` - **Create User**: `POST /scim/v2/{scimId}/Users` -- **Update User**: `PUT /scim/v2/{scimId}/Users/{userId}` +- **Update User**: `PUT /scim/v2/{scimId}/Users/{userId}` or `PATCH /scim/v2/{scimId}/Users/{userId}` - **Delete User**: `DELETE /scim/v2/{scimId}/Users/{userId}` +- **List Groups**: `GET /scim/v2/{scimId}/Groups` +- **Get Group**: `GET /scim/v2/{scimId}/Groups/{groupId}` +- **Create Group**: `POST /scim/v2/{scimId}/Groups` +- **Update Group**: `PUT /scim/v2/{scimId}/Groups/{groupId}` or `PATCH /scim/v2/{scimId}/Groups/{groupId}` +- **Delete Group**: `DELETE /scim/v2/{scimId}/Groups/{groupId}` ### Project SCIM User Lifecycle @@ -74,10 +81,12 @@ Status Page SCIM allows identity providers to manage subscribers to private stat ### Status Page SCIM Endpoints - **Service Provider Config**: `GET /status-page-scim/v2/{scimId}/ServiceProviderConfig` +- **Schemas**: `GET /status-page-scim/v2/{scimId}/Schemas` +- **Resource Types**: `GET /status-page-scim/v2/{scimId}/ResourceTypes` - **List Users**: `GET /status-page-scim/v2/{scimId}/Users` - **Get User**: `GET /status-page-scim/v2/{scimId}/Users/{userId}` - **Create User**: `POST /status-page-scim/v2/{scimId}/Users` -- **Update User**: `PUT /status-page-scim/v2/{scimId}/Users/{userId}` +- **Update User**: `PUT /status-page-scim/v2/{scimId}/Users/{userId}` or `PATCH /status-page-scim/v2/{scimId}/Users/{userId}` - **Delete User**: `DELETE /status-page-scim/v2/{scimId}/Users/{userId}` ### Status Page SCIM User Lifecycle @@ -91,45 +100,317 @@ Status Page SCIM allows identity providers to manage subscribers to private stat ## Identity Provider Configuration -### Azure Active Directory (Azure AD) +### Microsoft Entra ID (formerly Azure AD) -1. **Add OneUptime from Azure AD Gallery** - - In Azure AD, go to **Enterprise Applications** > **New Application** - - Add a **Non-gallery application** +Microsoft Entra ID provides enterprise-grade identity management with robust SCIM provisioning capabilities. Follow these detailed steps to configure SCIM provisioning with OneUptime. -2. **Configure SCIM Settings** - - In the OneUptime application, go to **Provisioning** - - Set **Provisioning Mode** to **Automatic** - - Enter the **Tenant URL** (SCIM Base URL from OneUptime) - - Enter the **Secret Token** (Bearer Token from OneUptime) - - Test the connection and save +#### Prerequisites -3. **Configure Attribute Mappings** - - Map Azure AD attributes to OneUptime SCIM attributes - - Ensure `userPrincipalName` or `mail` is mapped to `userName` - - Configure any additional attribute mappings as needed +- Microsoft Entra ID tenant with Premium P1 or P2 license (required for automatic provisioning) +- OneUptime account with Scale plan or higher +- Admin access to both Microsoft Entra ID and OneUptime -4. **Assign Users** - - Go to **Users and groups** and assign users to the OneUptime application - - Users will be automatically provisioned to OneUptime +#### Step 1: Get SCIM Configuration from OneUptime + +1. Log in to your OneUptime dashboard +2. Navigate to **Project Settings** > **Team** > **SCIM** +3. Click **Create SCIM Configuration** +4. Enter a friendly name (e.g., "Microsoft Entra ID Provisioning") +5. Configure the following options: + - **Auto Provision Users**: Enable to automatically create users + - **Auto Deprovision Users**: Enable to automatically remove users + - **Default Teams**: Select teams that new users should be added to + - **Enable Push Groups**: Enable if you want to manage team membership via Entra ID groups +6. Save the configuration +7. Copy the **SCIM Base URL** and **Bearer Token** - you'll need these for Entra ID + +#### Step 2: Create Enterprise Application in Microsoft Entra ID + +1. Sign in to the [Microsoft Entra admin center](https://entra.microsoft.com) +2. Navigate to **Identity** > **Applications** > **Enterprise applications** +3. Click **+ New application** +4. Click **+ Create your own application** +5. Enter a name (e.g., "OneUptime") +6. Select **Integrate any other application you don't find in the gallery (Non-gallery)** +7. Click **Create** + +#### Step 3: Configure SCIM Provisioning + +1. In your OneUptime enterprise application, go to **Provisioning** +2. Click **Get started** +3. Set **Provisioning Mode** to **Automatic** +4. Under **Admin Credentials**: + - **Tenant URL**: Enter the SCIM Base URL from OneUptime (e.g., `https://oneuptime.com/api/identity/scim/v2/{your-scim-id}`) + - **Secret Token**: Enter the Bearer Token from OneUptime +5. Click **Test Connection** to verify the configuration +6. Click **Save** + +#### Step 4: Configure Attribute Mappings + +1. In the Provisioning section, click **Mappings** +2. Click **Provision Azure Active Directory Users** +3. Configure the following attribute mappings: + +| Azure AD Attribute | OneUptime SCIM Attribute | Required | +|-------------------|-------------------------|----------| +| `userPrincipalName` | `userName` | Yes | +| `mail` | `emails[type eq "work"].value` | Recommended | +| `displayName` | `displayName` | Recommended | +| `givenName` | `name.givenName` | Optional | +| `surname` | `name.familyName` | Optional | +| `Switch([IsSoftDeleted], , "False", "True", "True", "False")` | `active` | Recommended | + +4. Remove any mappings that are not needed to simplify provisioning +5. Click **Save** + +#### Step 5: Configure Group Provisioning (Optional) + +If you enabled **Push Groups** in OneUptime: + +1. Go back to **Mappings** +2. Click **Provision Azure Active Directory Groups** +3. Enable group provisioning by setting **Enabled** to **Yes** +4. Configure the following attribute mappings: + +| Azure AD Attribute | OneUptime SCIM Attribute | +|-------------------|-------------------------| +| `displayName` | `displayName` | +| `members` | `members` | + +5. Click **Save** + +#### Step 6: Assign Users and Groups + +1. In your OneUptime enterprise application, go to **Users and groups** +2. Click **+ Add user/group** +3. Select the users and/or groups you want to provision to OneUptime +4. Click **Assign** + +#### Step 7: Start Provisioning + +1. Go to **Provisioning** > **Overview** +2. Click **Start provisioning** +3. The initial provisioning cycle will begin (this may take up to 40 minutes for the first sync) +4. Monitor the **Provisioning logs** for any errors + +#### Troubleshooting Microsoft Entra ID + +- **Test Connection Fails**: Verify the SCIM Base URL includes `/api/identity` prefix and the Bearer Token is correct +- **Users Not Provisioning**: Check that users are assigned to the application and attribute mappings are correct +- **Provisioning Errors**: Review the Provisioning logs in Entra ID for specific error messages +- **Sync Delays**: Initial provisioning can take up to 40 minutes; subsequent syncs occur every 40 minutes + +--- ### Okta -1. **SSO Application** - - You should already have the Okta application from the SSO integration you might have completed. If you do not, then please check out SSO Readme to create a new Okta App. +Okta provides flexible identity management with excellent SCIM support. Follow these detailed steps to configure SCIM provisioning with OneUptime. -2. **Configure SCIM Settings** - - In the application settings (General Tab), go to **Provisioning**, select SAML and click on Save. **Proviosning** tab should now be enabled. - - Set **SCIM connector base URL** to the OneUptime SCIM Base URL - - Set **Unique identifier field for users** to `userName` - - Enter the **Bearer Token** in the authentication header +#### Prerequisites -3. **Configure Attribute Mappings** - - Map Okta user attributes to SCIM attributes - - Ensure `email` is mapped to `userName` - - Configure additional mappings as needed +- Okta tenant with provisioning capabilities (Lifecycle Management feature) +- OneUptime account with Scale plan or higher +- Admin access to both Okta and OneUptime -4. **Assign Users** - - Assign users to the OneUptime application - - Users will be automatically provisioned to OneUptime +#### Step 1: Get SCIM Configuration from OneUptime + +1. Log in to your OneUptime dashboard +2. Navigate to **Project Settings** > **Team** > **SCIM** +3. Click **Create SCIM Configuration** +4. Enter a friendly name (e.g., "Okta Provisioning") +5. Configure the following options: + - **Auto Provision Users**: Enable to automatically create users + - **Auto Deprovision Users**: Enable to automatically remove users + - **Default Teams**: Select teams that new users should be added to + - **Enable Push Groups**: Enable if you want to manage team membership via Okta groups +6. Save the configuration +7. Copy the **SCIM Base URL** and **Bearer Token** - you'll need these for Okta + +#### Step 2: Create or Configure Okta Application + +**If you have an existing SSO application:** +1. Sign in to your Okta Admin Console +2. Navigate to **Applications** > **Applications** +3. Find and select your existing OneUptime application + +**If creating a new application:** +1. Sign in to your Okta Admin Console +2. Navigate to **Applications** > **Applications** +3. Click **Create App Integration** +4. Select **SAML 2.0** and click **Next** +5. Enter "OneUptime" as the App name +6. Complete the SAML configuration (refer to SSO documentation) +7. Click **Finish** + +#### Step 3: Enable SCIM Provisioning + +1. In your OneUptime application, go to the **General** tab +2. In the **App Settings** section, click **Edit** +3. Under **Provisioning**, select **SCIM** +4. Click **Save** +5. A new **Provisioning** tab will appear + +#### Step 4: Configure SCIM Connection + +1. Go to the **Provisioning** tab +2. Click **Integration** in the left sidebar +3. Click **Configure API Integration** +4. Check **Enable API integration** +5. Configure the following: + - **SCIM connector base URL**: Enter the SCIM Base URL from OneUptime (e.g., `https://oneuptime.com/api/identity/scim/v2/{your-scim-id}`) + - **Unique identifier field for users**: Enter `userName` + - **Supported provisioning actions**: Select the actions you want to enable: + - Import New Users and Profile Updates + - Push New Users + - Push Profile Updates + - Push Groups (if using group-based provisioning) + - **Authentication Mode**: Select **HTTP Header** + - **Authorization**: Enter `Bearer {your-bearer-token}` (replace with actual token) +6. Click **Test API Credentials** to verify the connection +7. Click **Save** + +#### Step 5: Configure Provisioning to App + +1. In the **Provisioning** tab, click **To App** in the left sidebar +2. Click **Edit** +3. Enable the following options: + - **Create Users**: Enable to provision new users + - **Update User Attributes**: Enable to sync attribute changes + - **Deactivate Users**: Enable to deprovision users when unassigned +4. Click **Save** + +#### Step 6: Configure Attribute Mappings + +1. Scroll down to **Attribute Mappings** +2. Verify or configure the following mappings: + +| Okta Attribute | OneUptime SCIM Attribute | Direction | +|---------------|-------------------------|-----------| +| `userName` | `userName` | Okta to App | +| `user.email` | `emails[primary eq true].value` | Okta to App | +| `user.firstName` | `name.givenName` | Okta to App | +| `user.lastName` | `name.familyName` | Okta to App | +| `user.displayName` | `displayName` | Okta to App | + +3. Remove any unnecessary mappings +4. Click **Save** if you made changes + +#### Step 7: Configure Push Groups (Optional) + +If you enabled **Push Groups** in OneUptime: + +1. Go to the **Push Groups** tab +2. Click **+ Push Groups** +3. Select **Find groups by name** or **Find groups by rule** +4. Search for and select the groups you want to push +5. Click **Save** + +#### Step 8: Assign Users + +1. Go to the **Assignments** tab +2. Click **Assign** > **Assign to People** or **Assign to Groups** +3. Select the users or groups you want to provision +4. Click **Assign** for each selection +5. Click **Done** + +#### Step 9: Verify Provisioning + +1. Go to **Reports** > **System Log** in the Okta Admin Console +2. Filter for events related to your OneUptime application +3. Verify that provisioning events are successful +4. Check OneUptime to confirm users have been created + +#### Troubleshooting Okta + +- **API Credentials Test Fails**: Verify the SCIM Base URL and Bearer Token are correct +- **Users Not Provisioning**: Ensure users are assigned to the application and provisioning is enabled +- **Duplicate Users**: Ensure the `userName` attribute is unique and maps correctly to email +- **Group Push Failures**: Verify groups exist and have the correct membership +- **Error: 401 Unauthorized**: Regenerate the Bearer Token in OneUptime and update Okta + +--- + +### Other Identity Providers + +OneUptime's SCIM implementation follows the SCIM v2.0 specification and should work with any compliant identity provider. General configuration steps: + +1. **SCIM Base URL**: `https://oneuptime.com/api/identity/scim/v2/{scim-id}` (for projects) or `https://oneuptime.com/api/identity/status-page-scim/v2/{scim-id}` (for status pages) +2. **Authentication**: HTTP Bearer Token +3. **Required User Attribute**: `userName` (must be a valid email address) +4. **Supported Operations**: GET, POST, PUT, PATCH, DELETE for Users and Groups + +#### Supported SCIM Endpoints + +| Endpoint | Methods | Description | +|----------|---------|-------------| +| `/ServiceProviderConfig` | GET | SCIM server capabilities | +| `/Schemas` | GET | Available resource schemas | +| `/ResourceTypes` | GET | Available resource types | +| `/Users` | GET, POST | List and create users | +| `/Users/{id}` | GET, PUT, PATCH, DELETE | Manage individual users | +| `/Groups` | GET, POST | List and create groups/teams (Project SCIM only) | +| `/Groups/{id}` | GET, PUT, PATCH, DELETE | Manage individual groups (Project SCIM only) | + +#### SCIM User Schema + +```json +{ + "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], + "userName": "user@example.com", + "name": { + "givenName": "John", + "familyName": "Doe", + "formatted": "John Doe" + }, + "displayName": "John Doe", + "emails": [ + { + "value": "user@example.com", + "type": "work", + "primary": true + } + ], + "active": true +} +``` + +#### SCIM Group Schema + +```json +{ + "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"], + "displayName": "Engineering Team", + "members": [ + { + "value": "user-id-here", + "display": "user@example.com" + } + ] +} +``` + +## Frequently Asked Questions + +### What happens when a user is deprovisioned? + +When a user is deprovisioned (either by DELETE request or by setting `active: false`), they are removed from the teams configured in the SCIM settings. The user account itself remains in OneUptime but loses access to the project. + +### Can I use SCIM without SSO? + +Yes, SCIM and SSO are independent features. You can use SCIM for user provisioning while allowing users to log in with their OneUptime passwords or any other authentication method. + +### How do I handle users who already exist in OneUptime? + +When SCIM tries to create a user who already exists (matching by email), OneUptime will simply add them to the configured default teams rather than creating a duplicate user. + +### What is the difference between default teams and push groups? + +- **Default Teams**: All users provisioned via SCIM are added to the same predefined teams +- **Push Groups**: Team membership is managed by your identity provider, allowing different users to be in different teams based on IdP group membership + +### How often does provisioning sync occur? + +This depends on your identity provider: +- **Microsoft Entra ID**: Initial sync can take up to 40 minutes, subsequent syncs every 40 minutes +- **Okta**: Near real-time for most operations, with periodic full syncs