Map IdP groups to roles in your app
Have your customer's identity provider send groups or a custom attribute, read them from the ID token in your SSO callback and set each user's role in your app.
Enterprise customers want access in your app to follow their directory: people in their “App Admins” group are admins in your app, and someone removed from the group loses that access. With Modular SSO, your app keeps its own roles. Scalekit passes along what the customer’s identity provider (IdP) says about the user, and your app turns that into a role at each login.
This page shows how to:
- Turn on the attribute in Scalekit: the built-in Groups attribute, or a custom one.
- Have your customer’s IT admin send it from their IdP.
- Read it from the ID token in your callback and set the user’s role.
It builds on the callback from Handle the callback and find the user.
Choose what the IdP sends
Section titled “Choose what the IdP sends”| Attribute | What the IdP sends | Use it when |
|---|---|---|
Groups (groups) | A list of the user’s group names, which your app maps to roles | The customer already manages access with groups. Most IdPs, including Okta and Microsoft Entra ID, can send groups |
A custom attribute, such as app_role | One value that names the role in your app, such as admin | The customer would rather set a role per user, or their IdP can’t send groups |
Your app can read both, as the code below does.
Turn on the attribute
Section titled “Turn on the attribute”Open User Attributes in the dashboard.
- Groups: edit Groups and turn on its SSO mapping. It’s a built-in attribute that is off until you turn it on.
- A custom attribute: select Add Attribute, enter an attribute name such as
app_roleand a display name, and save. Scalekit then reads an attribute or claim with the same name from the IdP.
Attributes you don’t add here aren’t passed on, even if the IdP sends them. Don’t name a custom attribute after a reserved name such as roles.
Have the IdP send it
Section titled “Have the IdP send it”Ask your customer’s IT admin to add the attribute to the app they created for your SSO connection:
- SAML: an attribute named
groups, or your custom attribute’s name, such asapp_role. In Okta, that’s a group attribute statement; in Microsoft Entra ID, a group claim. - OIDC: a
groupsclaim, or your custom attribute’s name, in the ID token or userinfo response.
The IT admin can check what their IdP sends with Test Connection in the admin portal. The result lists the attributes Scalekit received.
Set the role at each login
Section titled “Set the role at each login”The ID token from the code exchange carries groups as a list, and each custom attribute as a claim of its own. In your callback, after you validate the ID token, pass its claims to a function like this one and save the result on the user:
// Your app's role for each of a customer's IdP groups. Each customer names// its groups differently, so keep this map per organization.const GROUP_ROLES = { '<organization-id>': { 'App Admins': 'admin', 'App Editors': 'editor' },};const ROLES_BY_RANK = ['admin', 'editor', 'viewer'];
function roleFor(claims) { const groupRoles = GROUP_ROLES[claims.oid] ?? {}; const groups = Array.isArray(claims.groups) ? claims.groups : []; const roles = groups.map((group) => groupRoles[group]); // A custom attribute that names the role directly, if you use one if (ROLES_BY_RANK.includes(claims.app_role)) roles.push(claims.app_role); // The highest role wins; a user in no mapped group gets the lowest return ROLES_BY_RANK.find((role) => roles.includes(role)) ?? 'viewer';}
// In your callback, after: claims = await scalekit.validateToken(result.idToken);// user.role = roleFor(claims);# Your app's role for each of a customer's IdP groups. Each customer names# its groups differently, so keep this map per organization.GROUP_ROLES = { "<organization-id>": {"App Admins": "admin", "App Editors": "editor"},}ROLES_BY_RANK = ["admin", "editor", "viewer"]
def role_for(claims): group_roles = GROUP_ROLES.get(claims.get("oid"), {}) groups = claims.get("groups") roles = [group_roles.get(group) for group in groups] if isinstance(groups, list) else [] # A custom attribute that names the role directly, if you use one if claims.get("app_role") in ROLES_BY_RANK: roles.append(claims["app_role"]) # The highest role wins; a user in no mapped group gets the lowest return next((role for role in ROLES_BY_RANK if role in roles), "viewer")
# In your callback:# claims = scalekit_client.validate_token(result["id_token"])# user.role = role_for(claims)package main
import "slices"
// Your app's role for each of a customer's IdP groups. Each customer names// its groups differently, so keep this map per organization.var groupRoles = map[string]map[string]string{ "<organization-id>": {"App Admins": "admin", "App Editors": "editor"},}
var rolesByRank = []string{"admin", "editor", "viewer"}
func roleFor(claims map[string]interface{}) string { orgID, _ := claims["oid"].(string) var roles []string groups, _ := claims["groups"].([]interface{}) for _, g := range groups { if group, ok := g.(string); ok { roles = append(roles, groupRoles[orgID][group]) } } // A custom attribute that names the role directly, if you use one if appRole, ok := claims["app_role"].(string); ok && slices.Contains(rolesByRank, appRole) { roles = append(roles, appRole) } // The highest role wins; a user in no mapped group gets the lowest for _, role := range rolesByRank { if slices.Contains(roles, role) { return role } } return "viewer"}
// In your callback, after:// claims, err := scalekitClient.ValidateToken(r.Context(), result.IdToken)// user.Role = roleFor(claims)import java.util.ArrayList;import java.util.List;import java.util.Map;
public class Roles { // Your app's role for each of a customer's IdP groups. Each customer names // its groups differently, so keep this map per organization. static final Map<String, Map<String, String>> GROUP_ROLES = Map.of( "<organization-id>", Map.of("App Admins", "admin", "App Editors", "editor")); static final List<String> ROLES_BY_RANK = List.of("admin", "editor", "viewer");
static String roleFor(Map<String, Object> claims) { Map<String, String> groupRoles = GROUP_ROLES.getOrDefault((String) claims.get("oid"), Map.of()); List<String> roles = new ArrayList<>(); if (claims.get("groups") instanceof List<?> groups) { for (Object group : groups) { roles.add(groupRoles.get(String.valueOf(group))); } } // A custom attribute that names the role directly, if you use one if (claims.get("app_role") instanceof String appRole && ROLES_BY_RANK.contains(appRole)) { roles.add(appRole); } // The highest role wins; a user in no mapped group gets the lowest return ROLES_BY_RANK.stream().filter(roles::contains).findFirst().orElse("viewer"); }}
// In your callback, after:// claims = scalekitClient.authentication().validateAccessTokenAndGetClaims(result.getIdToken());// user.setRole(Roles.roleFor(claims));Keep these rules in the function:
- Set the role at every login, not only the first. When the IT admin removes someone from a group, the change reaches your app at the user’s next login.
- Map groups per organization. One customer’s “Admins” group says nothing about another’s. The ID token’s
oidclaim tells you whose groups they are. - Accept only roles your app defines. Ignore groups and values you haven’t mapped, and give users with none the lowest role.
Check it worked
Section titled “Check it worked”- Add a custom attribute named
app_role, as in Turn on the attribute. - Start a login for your test organization. In the SSO simulator, select + Add Attribute, enter
app_roleas the key andadminas the value, and select Simulate Login. - Your callback’s ID token claims include
"app_role": "admin", and the user’s role isadmin.
The simulator sends each value as text, so test Groups with a real IdP, such as a free Okta developer account.
Reserved attribute names
Section titled “Reserved attribute names”These names are used by Scalekit. A custom attribute with one of them is dropped or overwritten:
| Name | Use |
|---|---|
roles, permissions | Roles and permissions in Scalekit’s full-stack auth. In Modular SSO, send groups or a custom attribute such as app_role instead |
email, email_verified | Standard email claims |
name, given_name, family_name | Standard profile claims |
sub, oid, sid | Scalekit identifiers |
- Normalized user profile: the fields your callback receives, including the IdP’s raw attributes.
- Onboard an enterprise customer: create their organization and give their IT admin the admin portal.