Single sign-on configuration
When users sign in with Microsoft Entra ID, their role comes from the Entra ID security groups they belong to. A group role mapping links one group, identified by its object ID, to a role. At each sign-in the server compares the user's groups with the enabled mappings and applies the highest-ranked matching role. Roles, from lowest to highest rank, are default, explorative, expert, and admin. The admin role is a product role, not an administrator account: a mapping can never make a user an administrator of the installation, and administrators keep their role whatever their group membership.
Users who match no mapping receive the default role set in the Entra ID SSO section of platform settings, or are refused if that section requires a group match. By default the role is re-evaluated at every sign-in, so removing someone from a mapped group changes their role the next time they sign in. Mapping changes apply to sign-ins within about a minute. Users start single sign-on with Start Microsoft sign-in.
The tenant, application (client) ID and client secret are platform settings; Test the single sign-on configuration checks them. Role mappings and the connection test answer 402 while the installation's licence is restricted, for example after it has expired.
Administrator access. Every endpoint on this page requires an administrator account.
- GET /admin/sso/entra/mappings List group role mappings
- POST /admin/sso/entra/mappings Create a group role mapping
- PUT /admin/sso/entra/mappings/{mapping_id} Update a group role mapping
- DEL /admin/sso/entra/mappings/{mapping_id} Delete a group role mapping
- POST /admin/sso/entra/test-connection Test the single sign-on configuration
Paths are relative to /api
The group role mapping object
Links one Entra ID security group to the role its members receive at sign-in.
Attributes
-
idintegerUnique identifier of the mapping.
-
groupIdstringObject ID of the Entra ID security group, as a GUID.
-
groupLabelnullable stringLabel for the group, for your reference. It is not checked against the directory.
-
roleNamestringRole granted to members of the group:
default,explorative,expert, oradmin. -
isEnabledbooleanWhether the mapping is applied at sign-in.
-
createdAtstringWhen the mapping was created, in ISO 8601 with a UTC offset.
-
updatedAtstringWhen the mapping was last changed, in ISO 8601 with a UTC offset.
{
"id": 3,
"groupId": "8c1f4e2a-6b3d-4f7a-9e21-5d0c7b8a9f12",
"groupLabel": "Engineering experts",
"roleName": "expert",
"isEnabled": true,
"createdAt": "2026-09-01T09:30:00.412871+00:00",
"updatedAt": "2026-09-01T09:30:00.412871+00:00"
} List group role mappings
GET /api/admin/sso/entra/mappings
Returns every group role mapping.
Returns all mappings, enabled and disabled, ordered by label and then by group ID. Mappings without a label come last.
- Authentication
- Bearer token How it works
- Permission
- Requires an administrator account.
Parameters
No parameters.
Returns
Returns a mappings array of group role mapping objects.
Errors
- 403 The caller is not an administrator.
curl "$VDF_BASE_URL/api/admin/sso/entra/mappings" \
-H "Authorization: Bearer $VDF_ACCESS_TOKEN" const response = await fetch(`${process.env.VDF_BASE_URL}/api/admin/sso/entra/mappings`, {
headers: {
Authorization: `Bearer ${process.env.VDF_ACCESS_TOKEN}`,
},
});
if (!response.ok) throw new Error(`Request failed with status ${response.status}`);
const data = await response.json(); import os
import requests
response = requests.get(
f"{os.environ['VDF_BASE_URL']}/api/admin/sso/entra/mappings",
headers={"Authorization": f"Bearer {os.environ['VDF_ACCESS_TOKEN']}"},
timeout=30,
)
response.raise_for_status()
data = response.json() {
"success": true,
"mappings": [
{
"id": 3,
"groupId": "8c1f4e2a-6b3d-4f7a-9e21-5d0c7b8a9f12",
"groupLabel": "Engineering experts",
"roleName": "expert",
"isEnabled": true,
"createdAt": "2026-09-01T09:30:00.412871+00:00",
"updatedAt": "2026-09-01T09:30:00.412871+00:00"
},
{
"id": 5,
"groupId": "d42b7c90-1e5f-4a38-8b6d-3c2e1f0a9b87",
"groupLabel": "Platform publishers",
"roleName": "admin",
"isEnabled": false,
"createdAt": "2026-09-02T14:05:12.018344+00:00",
"updatedAt": "2026-09-08T11:47:39.550921+00:00"
}
]
} Create a group role mapping
POST /api/admin/sso/entra/mappings
Maps an Entra ID security group to a role.
Creates an enabled mapping for the group in group_id. Each group can have only one mapping: if the group already has one, its label and role are replaced and its existing id is returned, and whether it is enabled is left unchanged.
- Authentication
- Bearer token How it works
- Permission
- Requires an administrator account.
Body parameters application/json
-
group_idstring RequiredObject ID of the Entra ID security group, as a GUID such as
8c1f4e2a-6b3d-4f7a-9e21-5d0c7b8a9f12. -
role_namestringRole to grant:
default,explorative,expert, oradmin, in any letter case. A value that resolves to the administrator role is rejected; any other value the server does not recognise is stored asdefault. -
group_labelstringLabel to show for the group, up to 255 characters. An empty value is stored as
null.
Returns
Returns the id of the created or updated mapping.
Errors
- 400
group_idis not a GUID, orrole_nameresolves to the administrator role. - 403 The caller is not an administrator.
- 500 The mapping could not be saved, for example because
group_labelis longer than 255 characters.
curl -X POST "$VDF_BASE_URL/api/admin/sso/entra/mappings" \
-H "Authorization: Bearer $VDF_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"group_id": "8c1f4e2a-6b3d-4f7a-9e21-5d0c7b8a9f12",
"group_label": "Engineering experts",
"role_name": "expert"
}' const response = await fetch(`${process.env.VDF_BASE_URL}/api/admin/sso/entra/mappings`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VDF_ACCESS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
group_id: '8c1f4e2a-6b3d-4f7a-9e21-5d0c7b8a9f12',
group_label: 'Engineering experts',
role_name: 'expert',
}),
});
if (!response.ok) throw new Error(`Request failed with status ${response.status}`);
const data = await response.json(); import os
import requests
response = requests.post(
f"{os.environ['VDF_BASE_URL']}/api/admin/sso/entra/mappings",
headers={"Authorization": f"Bearer {os.environ['VDF_ACCESS_TOKEN']}"},
json={
"group_id": "8c1f4e2a-6b3d-4f7a-9e21-5d0c7b8a9f12",
"group_label": "Engineering experts",
"role_name": "expert",
},
timeout=30,
)
response.raise_for_status()
data = response.json() {
"success": true,
"id": 3
} Update a group role mapping
PUT /api/admin/sso/entra/mappings/{mapping_id}
Replaces a group role mapping.
Replaces every field of the mapping. Fields you omit take their defaults rather than keeping their current values: role_name becomes default, group_label is cleared, and the mapping is enabled. Send the complete mapping each time.
Each Entra ID group can have only one mapping, so group_id must not belong to another mapping.
- Authentication
- Bearer token How it works
- Permission
- Requires an administrator account.
Path parameters
-
mapping_idinteger RequiredID of the mapping.
Body parameters application/json
-
group_idstring RequiredObject ID of the Entra ID security group, as a GUID.
-
role_namestringRole to grant:
default,explorative,expert, oradmin, in any letter case. A value that resolves to the administrator role is rejected; any other value the server does not recognise is stored asdefault. -
group_labelstringLabel to show for the group, up to 255 characters. Omitted or empty clears the label.
-
is_enabledbooleanWhether the mapping is applied at sign-in. Send a JSON boolean.
isEnabledis accepted as an alias.
Returns
Returns a success marker.
Errors
- 400
group_idis not a GUID, orrole_nameresolves to the administrator role. - 403 The caller is not an administrator.
- 404 No mapping has this ID.
curl -X PUT "$VDF_BASE_URL/api/admin/sso/entra/mappings/5" \
-H "Authorization: Bearer $VDF_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"group_id": "d42b7c90-1e5f-4a38-8b6d-3c2e1f0a9b87",
"group_label": "Platform publishers",
"role_name": "admin",
"is_enabled": true
}' const response = await fetch(`${process.env.VDF_BASE_URL}/api/admin/sso/entra/mappings/5`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.VDF_ACCESS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
group_id: 'd42b7c90-1e5f-4a38-8b6d-3c2e1f0a9b87',
group_label: 'Platform publishers',
role_name: 'admin',
is_enabled: true,
}),
});
if (!response.ok) throw new Error(`Request failed with status ${response.status}`);
const data = await response.json(); import os
import requests
response = requests.put(
f"{os.environ['VDF_BASE_URL']}/api/admin/sso/entra/mappings/5",
headers={"Authorization": f"Bearer {os.environ['VDF_ACCESS_TOKEN']}"},
json={
"group_id": "d42b7c90-1e5f-4a38-8b6d-3c2e1f0a9b87",
"group_label": "Platform publishers",
"role_name": "admin",
"is_enabled": True,
},
timeout=30,
)
response.raise_for_status()
data = response.json() {
"success": true
} Delete a group role mapping
DEL /api/admin/sso/entra/mappings/{mapping_id}
Deletes the mapping, so membership of the group no longer grants its role at sign-in. To stop applying a mapping without losing it, update it with is_enabled set to false.
- Authentication
- Bearer token How it works
- Permission
- Requires an administrator account.
Path parameters
-
mapping_idinteger RequiredID of the mapping.
Returns
Returns a success marker.
Errors
- 403 The caller is not an administrator.
- 404 No mapping has this ID.
curl -X DELETE "$VDF_BASE_URL/api/admin/sso/entra/mappings/5" \
-H "Authorization: Bearer $VDF_ACCESS_TOKEN" const response = await fetch(`${process.env.VDF_BASE_URL}/api/admin/sso/entra/mappings/5`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${process.env.VDF_ACCESS_TOKEN}`,
},
});
if (!response.ok) throw new Error(`Request failed with status ${response.status}`);
const data = await response.json(); import os
import requests
response = requests.delete(
f"{os.environ['VDF_BASE_URL']}/api/admin/sso/entra/mappings/5",
headers={"Authorization": f"Bearer {os.environ['VDF_ACCESS_TOKEN']}"},
timeout=30,
)
response.raise_for_status()
data = response.json() {
"success": true
} Test the single sign-on configuration
POST /api/admin/sso/entra/test-connection
Checks that single sign-on is enabled and fully configured.
Checks the single sign-on settings on this server without contacting Microsoft Entra ID. All of the following must hold:
- single sign-on is enabled;
- the tenant, the application (client) ID and the client secret are all set;
- the tenants allowed to sign in are known, either because the tenant is a specific directory or because an explicit list of allowed tenants is configured.
Because nothing is sent to Microsoft, a wrong client secret or an unreachable sign-in authority only shows up at the first real sign-in.
The result always comes back with HTTP 200. When a check fails, success is false and error says which setting to fix. When all checks pass, the response reports the configured tenant, the tenants allowed to sign in, and the sign-in authority in use, which differs from the Microsoft global authority only for sovereign clouds. The client secret is never included in the response.
- Authentication
- Bearer token How it works
- Permission
- Requires an administrator account.
Parameters
No parameters.
Returns
Returns success and, when every check passes, tenantId, allowedTenants, and authorityHost; otherwise error.
Errors
- 403 The caller is not an administrator.
curl -X POST "$VDF_BASE_URL/api/admin/sso/entra/test-connection" \
-H "Authorization: Bearer $VDF_ACCESS_TOKEN" const response = await fetch(`${process.env.VDF_BASE_URL}/api/admin/sso/entra/test-connection`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VDF_ACCESS_TOKEN}`,
},
});
if (!response.ok) throw new Error(`Request failed with status ${response.status}`);
const data = await response.json(); import os
import requests
response = requests.post(
f"{os.environ['VDF_BASE_URL']}/api/admin/sso/entra/test-connection",
headers={"Authorization": f"Bearer {os.environ['VDF_ACCESS_TOKEN']}"},
timeout=30,
)
response.raise_for_status()
data = response.json() {
"success": true,
"tenantId": "3f2b8c1d-5a6e-4f70-9b21-0c4d5e6f7a8b",
"allowedTenants": [
"3f2b8c1d-5a6e-4f70-9b21-0c4d5e6f7a8b"
],
"authorityHost": "https://login.example.com"
}