feat: Add SCIM Schemas and ResourceTypes endpoints for project and status page

This commit is contained in:
Nawaz Dhandala
2026-01-12 12:47:52 +00:00
parent f74c003065
commit 50868ac8ea
4 changed files with 805 additions and 45 deletions

View File

@@ -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<void> => {
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<void> => {
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<JSONObject> = 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<JSONObject> = await Promise.all(groupsPromises);
// Paginate results
// Paginate results (startIndex is 1-based in SCIM)
const paginatedGroups: Array<JSONObject> = groups.slice(
(startIndex - 1) * count,
startIndex * count,
startIndex - 1,
startIndex - 1 + count,
);
logger.debug(

View File

@@ -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<void> => {
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<void> => {
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<JSONObject> = users.slice(
(startIndex - 1) * count,
startIndex * count,
startIndex - 1,
startIndex - 1 + count,
);
logger.debug(

View File

@@ -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;
}
};

View File

@@ -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