Manage workforce (Identity Center) users headlessly (M2M)

Manage your organization’s workforce (Identity Center) users from a backend service with no interactive login — using only a service access key (AK/SK) and STS. The pattern is AWS-faithful: your key assumes a role, and the role’s temporary credentials carry the permission to manage IdC users.

AK/SK  →  assume-role  →  ASIA temp creds  →  FGAI-HMAC + X-FGAI-Security-Token
       →  /identity/v1/organizations/{orgId}/identity-center/users…

Why a role (and not your key directly)? Workforce-user management is authorized on every call against identitycenter:User:{verb}. That permission is granted to a role which lives in your organization’s management account; you assume the role to act with it. This keeps your long-lived service key permission-light and follows the same shape as AWS IAM + STS.

Prerequisites: a service access key (see Integrate an external app with AK/SK) and the ability to sign requests with FGAI-HMAC-SHA256 (see IAM roles and STS AssumeRole). All calls below are one signed HTTPS request to https://api.flexgalaxy.ai (or http://api.dev.fgai.test in local dev).

The role lives in the management account

The WorkforceAdminRole always lives in your organization’s management account ({mgmtAccountId}), because that is where the Identity Center directory is anchored. Its FRN is therefore frn:{mgmtAccountId}:iam:role/WorkforceAdminRole and its permission policy grants identitycenter:User:* on frn:{mgmtAccountId}:identitycenter:user/*. Which path you follow depends on where your service principal lives:

Path

Your service principal is in…

You get the role by…

Primary — platform-seeded (external integrator)

your own tenant account (not the management account)

the platform seeds the role in the management account for you; its trust admits your principal’s FRN across accounts and requires an sts:ExternalId. You assume it cross-account.

Secondary — self-authored (management-account only)

the management account itself

you author the role, its managed policy, and the attachment yourself, then assume it.

The primary path is the norm for an external integrator: a service key is pinned to its own account, so it cannot author a role in the management account — the platform seeds that role. The self-author path is available only when your principal already lives in the management account.

Two load-bearing constraints

Read these first — getting either wrong yields a silent 403 IMPLICIT_DENY. They hold on both paths (the platform-seeded role already satisfies them).

  1. The permission is granted with a MANAGED policy, attached to the role. A role’s inline policy is not honored for this path. The grant is a customer-managed iam-policy attached with POST …/roles/{roleId}/policies.

  2. The policy Resource must match the Identity Center user FRN. It must be frn:{mgmtAccountId}:identitycenter:user/* (or *). A narrower or wrong Resource will not match and the call is denied.

Primary path — assume the platform-seeded role (external integrator)

Your service principal lives in your own tenant account. During onboarding the platform seeds the WorkforceAdminRole in your organization’s management account, with a trust policy that admits your principal FRN across accounts and requires a shared-secret sts:ExternalId. You do not author anything — you assume and act.

What onboarding gives you:

  • the role FRN — frn:{mgmtAccountId}:iam:role/WorkforceAdminRole

  • the externalId shared secret for the trust condition

  • your {orgId} (and its {mgmtAccountId})

1. Assume the role → temporary credentials

POST /identity/v1/sts/assume-role — signed with your static service key (AKIA), even though the role is in a different (management) account. This is a cross-account assume-role; the role’s trust policy is the gate.

{
  "roleFrn": "frn:{mgmtAccountId}:iam:role/WorkforceAdminRole",
  "roleSessionName": "workforce-batch-42",
  "durationSeconds": 3600,
  "externalId": "shared-secret-xyz"
}

The response returns ASIA… credentials (accessKeyId, secretAccessKey, sessionToken, expiration). durationSeconds is clamped to [900, role.maxSessionDuration]. A wrong or missing externalId returns 403 with no credentials.

2. Manage workforce users with the temporary credentials

Sign each call with the ASIA credentials (FGAI-HMAC-SHA256) and add the X-FGAI-Security-Token header set to the sessionToken.

/identity/v1/organizations/{orgId}/identity-center/users

Method

Path

Purpose

GET

…/identity-center/users

List workforce users

POST

…/identity-center/users

Invite a workforce user

GET

…/identity-center/users/{userId}

Read a workforce user

PUT

…/identity-center/users/{userId}

Update a workforce user

POST

…/identity-center/users/{userId}/disable | /enable

Disable / enable

POST

…/identity-center/users/{userId}/reset-password

Reset login credential

DELETE

…/identity-center/users/{userId}

Remove a workforce user

Each operation is checked against identitycenter:User:{verb}; with the seeded role’s managed policy, the check returns ALLOW.

Secondary path — self-author the role (management-account principal only)

Use this only if your service principal already lives in the management account. In that case {accountId} in the paths below is {mgmtAccountId}, so you can author the whole chain over public routes with your static key, then assume the role exactly as in step 1 above.

1. Create the managed permission policy

POST /identity/v1/accounts/{mgmtAccountId}/iam-policies

{
  "name": "WorkforceAdmin",
  "policyDocument": {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": "identitycenter:User:*",
        "Resource": "frn:{mgmtAccountId}:identitycenter:user/*"
      }
    ]
  }
}

Narrow Action to the exact verbs you need if you prefer: identitycenter:User:List, :Read, :Create, :Update, :Delete, :Disable, :Enable, :ResetPassword. The response returns the policy id.

2. Create the role with a trust policy that admits your service principal

POST /identity/v1/accounts/{mgmtAccountId}/roles

The trust policy answers “who may assume me”. Admit your own service principal by FRN, and require sts:AssumeRole. Recommended: add an sts:ExternalId condition (confused-deputy hardening) and pass the same value on assume-role.

{
  "name": "WorkforceAdminRole",
  "maxSessionDuration": 3600,
  "trustPolicy": {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Principal": { "FGAI": ["frn:{mgmtAccountId}:iam:user/{yourPrincipalId}"] },
        "Action": "sts:AssumeRole",
        "Condition": { "StringEquals": { "sts:ExternalId": "shared-secret-xyz" } }
      }
    ]
  }
}

The response returns the role id; the role FRN is frn:{mgmtAccountId}:iam:role/WorkforceAdminRole.

3. Attach the managed policy to the role

POST /identity/v1/accounts/{mgmtAccountId}/roles/{roleId}/policies

{ "policyId": "a1b2c3d4-…" }

This is the step that makes the role able to manage IdC users. (Attaching the policy inline on the role does not work — see constraint 1.)

4. Assume the role and manage users

Assume the role and manage users exactly as in the primary path (steps 1–2 above), using roleFrn = frn:{mgmtAccountId}:iam:role/WorkforceAdminRole.

How to find your mgmtAccountId

The role FRN, the policy Resource, and the {orgId} path all point at your organization’s management account. Resolve it from the organization: read your organization (GET /organizations/v1/{orgId}) — its management account id is the mgmtAccountId. For a self-author (management-account) principal this is simply your own account id.

Long-running jobs: session duration and re-assume

Assumed-role credentials are short-lived — durationSeconds, clamped to [900, role.maxSessionDuration] (default 3600). A long headless batch must re-assume the role before the current expiration and continue signing with the fresh ASIA credentials. Refresh a little ahead of expiry (e.g. at 80% of the lifetime) rather than waiting for a 401.

Failures

  • Missing/invalid signature → 401.

  • Role missing or its trust policy denies you (wrong principal or wrong externalId) → 403 (indistinguishable, so a caller cannot probe for roles in another account), with no credentials returned.

  • Permission not granted (no managed policy attached, or Resource doesn’t match) → 403 on the identity-center/users call.

See also