Skip to main content

Identity Methods

Since System.Identity 2.29.0 the model declares the security-critical operations of users, groups, clients and data permissions as CK methods. A method has a fixed contract - parameters, result, declared error codes and the roles that may call it - so that a caller finds it in the tenant schema and the Identity Service implements it against exactly that contract.

Handlers are added step by step

Version 2.29.0 declares the methods. The Identity Service serves them with handlers that arrive story by story: User.ChangePassword is the first one (see User.ChangePassword), User.ResetPassword the second (see User.ResetPassword), followed by the role methods SetRoles, AddRole and RemoveRole of the user (see User roles); then the group and data permission methods (see Groups and Data permissions) and the client secret methods (see Client secrets); then User.CreateUser and User.Merge, so every declared method now has a handler. A method that is declared but has no handler answers METHOD_HANDLER_NOT_AVAILABLE. The REST API of the Identity Service and octo-cli keep working unchanged. The Identity Service is released with the new model only after the handlers are in.

User​

MethodKindWho may call itParametersDeclared errors
ChangePassword-1instancethe user themselves (allowSelf)currentPassword, newPassword (both sensitive)PASSWORD_POLICY_VIOLATION, CURRENT_PASSWORD_INVALID, USER_LOCKED_OUT, SHADOW_USER_PASSWORD_NOT_ALLOWED
ResetPassword-1instanceUserManagementnewPassword (sensitive)PASSWORD_POLICY_VIOLATION, SHADOW_USER_PASSWORD_NOT_ALLOWED
SetRoles-1instanceUserManagementroleNames (list)ROLE_NOT_FOUND
AddRole-1instanceUserManagementroleNameROLE_NOT_FOUND, ROLE_ALREADY_ASSIGNED
RemoveRole-1instanceUserManagementroleNameROLE_NOT_FOUND, ROLE_NOT_ASSIGNED
Merge-1instanceUserManagementsourceUserNameSOURCE_USER_NOT_FOUND, CANNOT_MERGE_INTO_ITSELF, MERGE_FAILED
DisableTwoFactor-1instanceUserManagementnonenone (since System.Identity 2.30.0, see below)
CreateUser-1staticUserManagementuserName, email, firstName, lastName (optional), password (optional, sensitive)USER_NAME_RESERVED, USER_ALREADY_EXISTS, PASSWORD_POLICY_VIOLATION

ChangePassword is for the signed-in user and needs the current password; an administrator uses ResetPassword. The static create method is named CreateUser because the names Create, Update and Delete are reserved for the generic mutations. CreateUser is the only way besides the REST API to create a user, see below. The model does not mark it with replacesGenericCreate yet (that is a major version change of the model); the platform enforces the same rule.

Group​

MethodWho may call itParametersDeclared errors
AddMember-1UserManagementmemberKind (User or Client), memberId (rtId of the user, client id of the client)MEMBER_NOT_FOUND, MEMBER_ALREADY_IN_GROUP
RemoveMember-1UserManagementmemberKind, memberIdMEMBER_NOT_FOUND, MEMBER_NOT_IN_GROUP
SetRoles-1UserManagementroleNames (list)ROLE_NOT_FOUND

Client​

MethodWho may call itParametersResultDeclared errors
AddSecret-1TenantManagement, UserManagementdescription, expirationDate (both optional)the generated secret (sensitive, shown once)CLIENT_SECRET_NOT_REQUIRED
RemoveSecret-1TenantManagement, UserManagementsecretValue (sensitive)-SECRET_NOT_FOUND

Data permission​

MethodWho may call itParametersDeclared errors
GrantToRole-1UserManagementroleNameROLE_NOT_FOUND, PERMISSION_ALREADY_GRANTED
RevokeFromRole-1UserManagementroleNameROLE_NOT_FOUND, PERMISSION_NOT_GRANTED

Rules that apply to all of them​

  • Roles: the roles are the ones of the REST API (UserManagement; for client secrets also TenantManagement). Calling a method also needs the octo_api scope; without a matching role the call is refused before the target record is read.
  • Sensitive values: every parameter that carries a password or a secret is declared sensitive, and so is the result of AddSecret. Such values never appear in logs, audit entries or traces. A sensitive parameter cannot be handled by a pipeline.
  • Data access: every method declares dataAccess: Write: the caller needs a write grant on the target record (method authorization).
  • Generic create stays available for groups, clients and data permissions: the System.Identity.Bootstrap blueprint seeds them, and a replaced generic create would make the bootstrap of every new tenant fail.
  • Versioning: adding the methods is an additive change, a minor version (2.28.0 to 2.29.0).

How the Identity Service serves methods​

The Identity Service hosts its methods as a remote handler on the message bus: the gateway of the asset repository forwards a call with the access token of the caller, the Identity Service validates that token again (issuer, signature, expiry, tenant and the octo_api scope, against its own authority) and only then runs the handler. A call without a valid token is answered METHOD_FORBIDDEN. Every handler runs in the tenant of the call and writes one entry per call (success or failure) into the event log of the tenant; the entry carries identifiers and the outcome code, never a parameter value.

Switch for methods that carry a password or secret​

SettingDefaultMeaning
Methods:Identity:EnablePasswordMethods (environment variable OCTO_Methods__Identity__EnablePasswordMethods)falseRegisters the handlers of methods with a sensitive parameter or result (User.ChangePassword, User.ResetPassword, Client.AddSecret, Client.RemoveSecret, User.CreateUser).

With the default the Identity Service does not announce these methods and the gateway answers METHOD_HANDLER_NOT_AVAILABLE; the REST API is unaffected. The switch is read at start, so restart the service after changing it.

Not on staging and production yet

The method queues carry the parameters. No method with a password or secret may be enabled on staging-1, prod-1 and prod-2 before the restricted broker user (B0) is rolled out. Local environments and test-2 may enable it with test accounts. For the Helm deployment set the value as an environment variable of the identity service (OCTO_Methods__Identity__EnablePasswordMethods=true); leave it unset elsewhere.

User.ChangePassword​

A signed-in user changes their own password. The call needs the current password (currentPassword) and the new one (newPassword).

SituationAnswer
successthe password is changed, the security stamp rotates and all refresh tokens and authorizations of the user in this tenant are revoked (other sessions end)
the target is another user, also for an administrator or a service tokenMETHOD_FORBIDDEN (reason = not-self); the answer is the same whether the target exists or not. An administrator uses ResetPassword.
wrong current passwordCURRENT_PASSWORD_INVALID; counts like a failed sign-in towards the lockout
the user is locked outUSER_LOCKED_OUT (also with a correct current password)
the new password violates the policyPASSWORD_POLICY_VIOLATION with one detail per failed rule (code only, for example PasswordTooShort, PasswordRequiresDigit); never the password
cross-tenant shadow userSHADOW_USER_PASSWORD_NOT_ALLOWED (the password is changed in the home tenant)

The policy is the one of ASP.NET Core Identity (minimum length 6, digit, lower case, upper case, non-alphanumeric); a client branches on the detail code, not on a text.

User roles​

SetRoles, AddRole and RemoveRole change the direct roles of a user. They follow the rules of the REST endpoints (PUT /users/{user}/roles, PUT and DELETE /users/{user}/roles/{role}) and never allow more than REST: whoever may not change a role over REST may not change it with a method either. Roles that a user inherits from a group are neither read nor written. These methods carry no password or secret, so they are not behind the switch above and are available whenever the Identity Service runs.

MethodWhat it doesAnswers
SetRoles (roleNames)replaces the direct roles by the given list; an empty list removes all direct rolesROLE_NOT_FOUND if a name is unknown: then nothing is changed
AddRole (roleName)adds one roleROLE_NOT_FOUND, ROLE_ALREADY_ASSIGNED
RemoveRole (roleName)removes one roleROLE_NOT_FOUND, ROLE_NOT_ASSIGNED

The rules, side by side with REST:

RuleRESTMethod
the caller needs the tenant role UserManagement and the scope octo_api (the read-only scope is not enough)403METHOD_FORBIDDEN; the Identity Service checks this again on the validated access token, so a forged message on the bus is refused as well
the user must exist404METHOD_TARGET_NOT_FOUND
the role must exist (names are not case sensitive)404ROLE_NOT_FOUND, the parameter roleName names the role
a role that is already assigned cannot be added400ROLE_ALREADY_ASSIGNED
a role that is not assigned cannot be removed400ROLE_NOT_ASSIGNED
service clients are judged by their client rolesallowed with UserManagementallowed with UserManagement

REST and the methods also share the rules against escalation, which are described with the roles in Users and Roles. A refused call answers METHOD_FORBIDDEN with the parameter reason (no further error code is declared, the model is unchanged):

reasonMeaningREST
role-requiredthe caller lacks the role UserManagement or the scope octo_api403
self-grantthe caller tried to add a direct role to their own user403, code = self-grant
role-not-heldthe caller tried to grant a built-in role they do not hold; the parameter roleNames names the roles403, code = role-not-held, roleNames
last-administratorthe change would leave the tenant without a user who holds UserManagement409, code = last-administrator

SetRoles is judged on the difference to the current direct roles. One unknown role name changes nothing in either transport (ROLE_NOT_FOUND; REST answers 404).

Like in REST, a role change does not end running sessions: the user's next token carries the new roles. Every call, including every refusal, writes one entry into the event log of the tenant (category Identity.Method) with the calling user, the target, the outcome (for a refusal also the reason and the refused roleNames) and the direct roles before and after the call.

User.DisableTwoFactor​

A user administrator (role UserManagement) switches off the second factor of a user, for example when the user lost the authenticator app or the authenticator key did not survive a restore. Declared since System.Identity 2.30.0. The method carries no password or secret, so it is served regardless of Methods:Identity:EnablePasswordMethods. The REST endpoint POST {tenantId}/v1/users/{userName}/disableTwoFactor does the same and answers { "twoFactorWasEnabled": true|false }.

SituationAnswer
successthe authenticator key and the recovery codes are removed, TwoFactorEnabled is false, the security stamp rotates and all refresh tokens and authorizations of the user in this tenant are revoked. The user signs in with the password and can set up a new authenticator in the account settings. A user without a second factor gets the same treatment.
the caller does not have the role UserManagement (or the scope octo_api)METHOD_FORBIDDEN; the answer is the same whether the target exists or not
the target does not exist (authorized caller only)METHOD_TARGET_NOT_FOUND
a write or the revocation failsMETHOD_HANDLER_FAILED; never a success while the key or old refresh tokens survive

Audit. Every call writes one entry into the event log of the tenant (category Identity.Method) with the caller, the target, the outcome and twoFactorWasEnabled, and the Identity Service writes one log line naming tenant, user, caller and path. No key, recovery code or token is ever logged. Setting TwoFactorEnabled through a generic GraphQL mutation or an import is refused, see Fields Only the Identity Service Writes.

User.CreateUser​

A user administrator (role UserManagement) creates a user, with an initial password or as an invitation without one. Static method: it is called on the type, not on a user. The method has a sensitive parameter (password), so it is served only when Methods:Identity:EnablePasswordMethods is true, also for an invitation without a password; with the default false the gateway answers METHOD_HANDLER_NOT_AVAILABLE. The REST endpoint POST {tenantId}/v1/users does the same; both are the only ways to create a local user (see Creating Users).

SituationAnswer
successthe user is created through the user manager of the Identity Service: password policy (checked before the user is stored), normalized user name and e-mail, new security stamp. With password the user can sign in at once and gets the e-mail confirmation; without it (or with a blank one) the user gets the welcome mail with a link to set a password. The result is the id (rtId) of the new user, as a plain string in result ("result": "6ae7c66ccc5a608ccf779df9").
the user name starts with xt_ (reserved for cross-tenant users)USER_NAME_RESERVED
a user with this name (any casing) or this e-mail address existsUSER_ALREADY_EXISTS with the parameter field = userName or email. The REST endpoint POST {tenantId}/v1/users applies the same rule (409, code: USER_ALREADY_EXISTS, field: userName or email; before release 3.5.0 a duplicate user name answered 400), also when an existing user is renamed or changes to the address (PUT {tenantId}/v1/users/{userName}).
the password violates the password policyPASSWORD_POLICY_VIOLATION; the details name the failed rules (for example PasswordTooShort), never the value. Nothing is stored.
the user name is missing or contains characters the Identity Service does not accept, or the e-mail address is invalidMETHOD_VALIDATION_FAILED with the parameter field
the caller does not have the role UserManagement (or the scope octo_api)METHOD_FORBIDDEN (the gateway refuses it before the call; the Identity Service checks again)

Audit. Every call writes one entry into the event log of the tenant (category Identity.Method) with the caller, the outcome, the user name, whether a password was given (withPassword) and, on success, the id of the new user. The password is never logged, audited or returned in an error.

Generic create. A GraphQL create (generic, typed or a runtime-query row), an MCP create, a pipeline insert, a user-started ImportRt and the seed data of a blueprint refuse a new user with GENERIC_CREATE_REPLACED_BY_METHOD and the method id System.Identity/User.CreateUser-1; an existing user can still be updated (within Fields Only the Identity Service Writes).

User.Merge​

A user administrator (role UserManagement) merges a duplicate user into another one, for example a local account and the account an external sign-in created for the same person. The method is called on the user that is kept (the target) and names the duplicate in sourceUserName. It carries no password or secret, so it is served regardless of Methods:Identity:EnablePasswordMethods. The REST endpoint POST {tenantId}/v1/users/{userName}/merge (body { "sourceUserName" }) runs the same code and leaves the same state behind (see Merging Duplicate Users).

SituationAnswer
successthe external logins of the source user (for example Google or Microsoft) move to the target user, all refresh tokens, authorizations and browser sessions of the source user in this tenant are revoked, and the source user is deleted. The next sign-in with a moved login finds the target user. Roles, group memberships, claims and cross-tenant mappings of the source user are not transferred; they go away with the source user, so a merge never gives the target user a role.
the source user does not exist, or the caller cannot read it (data permissions)SOURCE_USER_NOT_FOUND
source and target are the same userCANNOT_MERGE_INTO_ITSELF
sourceUserName is missingMETHOD_VALIDATION_FAILED
the caller may read the source user but not change it (data permissions)METHOD_FORBIDDEN with the parameter reason = data-permission
the source user is the last user who holds UserManagementMETHOD_FORBIDDEN with reason = last-administrator (REST: 409), see Rules for Changing the Roles of a User
the caller does not have the role UserManagement (or the scope octo_api)METHOD_FORBIDDEN; the answer is the same whether the target exists or not
the target does not exist (authorized caller only)METHOD_TARGET_NOT_FOUND
a login could not be moved or the source user could not be deletedMERGE_FAILED with the parameter step (removeLogin, addLogin or deleteSource), loginProvider for the login steps, and the error codes of the user manager as details

A refused call changes nothing.

Audit. Every call writes one entry into the event log of the tenant (category Identity.Method) with the caller, the target, the outcome, the source user (sourceUserName, sourceRtId) and, after a merge, the number of moved logins (transferredLogins) and revoked tokens (revokedTokens); a refusal adds reason. The Identity Service also writes one log line for the revocation naming tenant, source user and caller. No token is ever logged or audited.

User.ResetPassword​

A user administrator (role UserManagement) sets a new password without the current one, also for a user who is locked out after too many failed sign-ins. The call needs only the new password (newPassword). Like every method that carries a password it is served only when Methods:Identity:EnablePasswordMethods is true; with the default (false) the gateway answers METHOD_HANDLER_NOT_AVAILABLE.

SituationAnswer
successthe password is changed, the security stamp rotates, all refresh tokens and authorizations of the user in this tenant are revoked (every session ends) and an active lockout is lifted (LockoutEnd is cleared, the failed-sign-in counter is 0). The user can sign in with the new password at once.
the caller does not have the role UserManagement (also a user resetting their own password, or a forged call on the bus)METHOD_FORBIDDEN; the answer is the same whether the target exists or not
the target does not exist (authorized caller only)METHOD_TARGET_NOT_FOUND
the new password violates the policyPASSWORD_POLICY_VIOLATION with one detail per failed rule (code only, for example PasswordTooShort); never the password. Nothing is changed, a lockout stays.
cross-tenant shadow userSHADOW_USER_PASSWORD_NOT_ALLOWED (the password is changed in the home tenant)

Audit. Every call writes one entry into the event log of the tenant (category Identity.Method), for success and for every failure: the tenant, the caller (callerSubject: user id or client id), the target (targetRtId), the outcome (SUCCESS or the error code) and lockoutLifted (true when the call lifted an active lockout, otherwise false). The password is never part of a log line, an audit entry or a trace.

Difference to the REST endpoint. POST system/v1/users/ResetPassword does not lift a lockout: the user stays locked out until the lockout time has passed, and the endpoint e-mails the user a second reset token. The method lifts the lockout and sends no e-mail; a notification to the user is not part of the method yet. The REST endpoint is unchanged.

Groups​

The Identity Service serves Group.AddMember, Group.RemoveMember and Group.SetRoles with the rules of the REST endpoints system/v1/groups/{group}/members/... and .../roles, never with more. They are hosted whatever EnablePasswordMethods says: they carry no password or secret.

MethodWhat it doesAnswers
AddMember (memberKind, memberId)adds a user or a client to the groupMEMBER_NOT_FOUND, MEMBER_ALREADY_IN_GROUP
RemoveMember (memberKind, memberId)removes a user or a client from the groupMEMBER_NOT_FOUND, MEMBER_NOT_IN_GROUP
SetRoles (roleNames)replaces the roles of the group by the given list; an empty list removes all rolesROLE_NOT_FOUND if a name is unknown: then nothing is changed

memberId is the id of the user or of the client (the route parameter of REST); for a client the OAuth client id is accepted as well. SetRoles takes role names (not case sensitive); REST takes role ids.

The rules, side by side with REST:

RuleRESTMethod
the caller needs the tenant role UserManagement and the scope octo_api403METHOD_FORBIDDEN (reason = role-required); checked again on the validated access token, so a forged message on the bus is refused
the group must exist404METHOD_TARGET_NOT_FOUND
the member or role must exist, and the caller must be allowed to read itan unhandled engine errorMEMBER_NOT_FOUND, ROLE_NOT_FOUND
a member cannot be added twice, a non-member cannot be removedsilent no-opMEMBER_ALREADY_IN_GROUP, MEMBER_NOT_IN_GROUP
a caller cannot give themselves a role (also not through a group they belong to)403, code = self-grantMETHOD_FORBIDDEN, reason = self-grant
a caller cannot give a built-in role they do not hold themselves403, code = role-not-heldMETHOD_FORBIDDEN, reason = role-not-held, roleNames
the change cannot leave the tenant without a user who holds UserManagement (judged over direct roles, groups, nested groups and mappings)409, code = last-administratorMETHOD_FORBIDDEN, reason = last-administrator
removing a client is not judged (a client gives nothing to anybody)allowedallowed

The rules against escalation are the same code as for the roles of a user, see Users and Roles. Like in REST, a change does not end running sessions: the member's next token carries the roles of the group as they are then. Every call, including every refusal, writes one entry into the event log of the tenant (category Identity.Method) with the calling user, the group, the outcome, memberKind and memberId (for SetRoles the role names before and after, for a refusal also the reason).

Data permissions​

DataPermission.GrantToRole and DataPermission.RevokeFromRole grant or take away a data permission for a role by role name (not case sensitive), with the rules of POST and DELETE system/v1/dataPermissions/{permissionId}/roles/{roleName}. The data permission is the target of the call.

SituationAnswer
successthe role holds (or no longer holds) the permission
the caller lacks the role UserManagement or the scope octo_apiMETHOD_FORBIDDEN (reason = role-required)
the data permission does not existMETHOD_TARGET_NOT_FOUND (REST: an unhandled error)
the role does not exist, or the caller may not read itROLE_NOT_FOUND
the caller may read the role but not change itMETHOD_FORBIDDEN (reason = data-permission)
GrantToRole for a role that already holds the permissionPERMISSION_ALREADY_GRANTED (REST: no-op)
RevokeFromRole for a role that does not hold itPERMISSION_NOT_GRANTED (REST: no-op)

A data permission grants access to data, not roles: like REST, the methods apply no escalation rules to it. The role is checked with the data permissions of the caller; as long as no policy protects the role type, the check is open, as in REST. Every call writes one entry into the event log of the tenant (category Identity.Method) with permissionId and roleName.

Client secrets​

Client.AddSecret generates a new secret for a client and returns it once as the result of the call; Client.RemoveSecret removes a secret again. The client is the target of the call (its record id). Both carry a secret, so they are served only when Methods:Identity:EnablePasswordMethods is true; with the default (false) the gateway answers METHOD_HANDLER_NOT_AVAILABLE. They follow the REST endpoints POST and POST system/v1/apiSecrets/client/{clientId}/delete (stored hash in the request body), which stay unchanged.

MethodWhat it doesAnswers
AddSecret (description, expirationDate, both optional)generates a secret of 256 random bits (URL-safe Base64), stores only its SHA-256 hash and returns the secretCLIENT_SECRET_NOT_REQUIRED for a public client (it never authenticates with a secret); METHOD_VALIDATION_FAILED for an expiration in the past (field = expirationDate) or the reserved description octo:mirror-own-secret (field = description)
RemoveSecret (secretValue)removes the secret; secretValue is either the stored value, as the list of secrets shows it, or the secret itselfSECRET_NOT_FOUND when the client has no such secret (the answer never repeats the value)
  • Shown once. The result of AddSecret is the only place where the secret appears. It is declared sensitive: the gateway hands it to the caller but never writes it into a log, an audit entry or a trace. Store it right away (for example in a vault); if it is lost, add a new secret and remove the old one.
  • Effect. A client that is configured for the client credentials grant can request tokens with the new secret at once (AddSecret does not change the allowed grant types). After RemoveSecret, a token request with the removed secret fails at once. Other secrets of the client keep working, so a secret can be rotated without a gap: add the new one, switch the caller, remove the old one.
  • Mirrors. For a client that is mirrored into child tenants the change is copied to the mirrors after the write. If that copy fails, the call still succeeds (the change is stored); every Identity Service instance repairs the mirrors on its next periodic check, by default within 5 minutes, so a removed secret is then refused in the child tenants too (Mirrored clients).
  • Callers. The caller needs the scope octo_api and the tenant role TenantManagement or UserManagement, as for REST. The Identity Service checks this again on the validated access token; without it the call is refused with METHOD_FORBIDDEN (reason = role-required), the same answer whether the client exists or not. An unknown client is METHOD_TARGET_NOT_FOUND.
  • Audit. Every call that reaches the Identity Service, including every refusal there, writes one entry into the event log of the tenant (category Identity.Method) with the caller, the target (targetRtId) and the outcome; once the client was found also clientId, after a write secretCount, and for RemoveSecret removedCount. A call the gateway refuses before it is dispatched is recorded by the gateway (Method.Invocation), not here. The secret, its hash and the description are never part of an entry.