Skip to Content
TutorialsTeam authorization

Build organization and team authorization

This tutorial gives one Identity subject the tickets.reply permission as a member of a support team. It demonstrates Identity’s directory contract; your product still owns customer records, subscriptions and business objects.

The model

The role declares which permissions travel together. The assignment states where that role applies.

Before you start

You need:

  • the exact tenant issuer;
  • an access token issued by that tenant;
  • audience equal to {issuer}/identity/directory;
  • scope directory.manage;
  • the matching live directory-management permission;
  • the subject of an active user in that tenant.
export ISSUER='https://auth.example.com' export DIRECTORY_URL="$ISSUER/identity/directory" export ACCESS_TOKEN='TENANT_DIRECTORY_ACCESS_TOKEN' export SUBJECT='usr_01JY7N8SV4B30TQGB8Y3YH5J9M'

A control-tenant Management API token cannot administer an unrelated tenant directory.

1. Create the organization

ISSUER='https://auth.example.com'
curl --fail --silent --show-error \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  --data '{"slug":"engineering","displayName":"Engineering"}' \
  "$ISSUER/identity/directory/organizations"

Save the returned UUID:

{ "id": "2a546f88-e746-4cdf-a87b-300a236d4788" }

The route uses the organization’s stable slug, while role-assignment bodies use its UUID.

2. Create the team

team_json="$( curl --fail-with-body --silent --show-error \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ --data '{"slug":"support","displayName":"Customer support"}' \ "$DIRECTORY_URL/organizations/engineering/teams" )" team_id="$(printf '%s' "$team_json" | jq -er '.id')" printf '%s\n' "$team_json" | jq

A team belongs to exactly one directory organization.

3. Add the subject to the organization and team

Organization membership is the parent invariant. Create it first:

curl --fail-with-body --silent --show-error \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ --data "{\"subject\":\"$SUBJECT\"}" \ "$DIRECTORY_URL/organizations/engineering/members" curl --fail-with-body --silent --show-error \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ --data "{\"subject\":\"$SUBJECT\"}" \ "$DIRECTORY_URL/organizations/engineering/teams/support/members"

Both operations return 204 No Content. Adding a team member without a valid parent organization membership is rejected with 409 directory_operation_rejected.

4. Define the permission

curl --fail-with-body --silent --show-error \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ --data '{"key":"tickets.reply","displayName":"Reply to support tickets"}' \ "$DIRECTORY_URL/permissions"

Success is 201 Created with an empty body. Permission keys are stable machine identifiers; display names may be presented to operators.

5. Create the team-scoped role

role_json="$( curl --fail-with-body --silent --show-error \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ --data '{ "key": "support-agent", "displayName": "Support agent", "scope": "TEAM" }' \ "$DIRECTORY_URL/roles" )" printf '%s\n' "$role_json" | jq

The role scope is part of its contract. A TEAM role cannot be assigned with tenant or organization context.

Attach the permission:

curl --fail-with-body --silent --show-error \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ --data '{"key":"tickets.reply"}' \ "$DIRECTORY_URL/roles/support-agent/permissions"

Success is 204 No Content.

6. Assign the role in the team

Use the organization ID returned in step 1 and the team ID returned in step 2:

organization_id='2a546f88-e746-4cdf-a87b-300a236d4788' curl --fail-with-body --silent --show-error \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ --data "{ \"roleKey\": \"support-agent\", \"subject\": \"$SUBJECT\", \"scope\": \"TEAM\", \"organizationId\": \"$organization_id\", \"teamId\": \"$team_id\" }" \ "$DIRECTORY_URL/role-assignments"

Success is 204 No Content.

7. Read back the effective directory facts

Read each bounded collection and continue until last is true:

curl --fail-with-body --silent --show-error \ -H "Authorization: Bearer $ACCESS_TOKEN" \ "$DIRECTORY_URL/organizations/engineering/teams/support/members?page=0&size=25" | jq curl --fail-with-body --silent --show-error \ -H "Authorization: Bearer $ACCESS_TOKEN" \ "$DIRECTORY_URL/roles/support-agent/permissions?page=0&size=25" | jq curl --fail-with-body --silent --show-error \ -H "Authorization: Bearer $ACCESS_TOKEN" \ "$DIRECTORY_URL/role-assignments?page=0&size=25" | jq

Verify the exact subject, role key, TEAM scope and both context IDs.

Security boundary

A directory assignment authorizes an Identity-side capability. Before replying to a ticket, the product API must still prove that the ticket belongs to the corresponding product organization and that the account is active there.

Revocation behavior

To revoke the role, send the same assignment body to POST /role-assignments/revocations. Removing the team member also removes team relationships that depend on that membership. Removing the organization member removes dependent team memberships and organization or team role assignments.

Treat revocation as a live authorization change. Do not keep a product-side permission cache longer than the delay your security policy allows.

Verify the finished use case

  • the team cannot contain a subject outside its parent organization;
  • the role scope and assignment scope match;
  • all paginated reads contain the expected active records;
  • revoking the assignment removes access without changing the user’s authentication account;
  • removing the organization membership removes dependent access;
  • product authorization still checks its own organization and business rules.

See the Directory API contract for every request body and error.