Identity Access Management Patterns

Production-grade guide to identity access management patterns covering architecture patterns, implementation strategies, testing approaches, and operational best practices for enterprise engineering teams.

Identity Access Management (IAM) patterns are reusable solutions for managing digital identities and controlling access to resources across systems, services, and environments. They are essential when an organisation has multiple applications, users, teams, and external partners, and must enforce consistent policies across systems that may use different protocols, credentials, and scopes. IAM patterns solve the recurring problem of *how to authenticate users, authorise their actions, and manage their lifecycles*—not just in one app, but across the entire stack.

Centralised Identity Provisioning

User Lifecycle Management via Events and Workflows

User provisioning is the process of creating, updating, and deactivating identities in downstream systems. It begins when a user is added to a central identity store—typically a directory service like LDAP or a cloud identity provider—and propagates to all integrated applications.

A user record is created in the identity provider (IdP) with attributes such as user_id, email, department, manager, and status. When a new user is added, an event is emitted: { "event": "user.created", "user_id": "u-1234", "email": "[email protected]", "department": "engineering" }.

This event triggers a workflow that synchronises the user to external systems. For example, a user_provisioner service consumes this event and performs the following steps:

curl -X POST https://api.idp.acme.com/events \
  -H "Content-Type: application/json" \
  -d '{
    "event": "user.created",
    "user_id": "u-1234",
    "email": "[email protected]",
    "department": "engineering",
    "timestamp": "2024-03-15T12:00:00Z"
  }'

The user_provisioner service reads a configuration file provisioning.yaml:

services:
  - name: "auth-service"
    endpoint: "https://auth.acme.com/v1/users"
    auth:
      type: "bearer"
      token: "sso:auth:prod"
    mapping:
      user_id: "user_id"
      email: "email"
      department: "department"
      status: "status"
    conditions:
      - "department == 'engineering'"
      - "status == 'active'"

On receiving the event, the service makes a POST request to the target API:

POST /v1/users HTTP/1.1
Host: auth.acme.com
Authorization: Bearer sso:auth:prod
Content-Type: application/json

{
  "user_id": "u-1234",
  "email": "[email protected]",
  "department": "engineering",
  "status": "active"
}

The service handles common failure modes:

A common silent failure is stale user data: a user’s department is updated in the IdP, but the user_provisioner runs only once daily. The department field in the auth-service remains outdated until the next sync. To fix this, the service must also listen for user.updated events.

Synchronized Attributes via Schema Mapping

Attributes in the IdP may not match those in downstream systems. The mapping between user.department in the IdP and user.dept in the auth-service must be explicit. A mismatched field (e.g., user.location → user.region) silently breaks role assignment.

The service must validate that all required fields are present. If email is missing, it logs:

{
  "event_id": "evt-9876",
  "service": "auth-service",
  "error": "missing_required_field",
  "field": "email",
  "timestamp": "2024-03-15T12:01:30Z",
  "payload": {
    "user_id": "u-1234",
    "department": "engineering"
  }
}

When a user is deactivated in the IdP, the user_provisioner updates the user’s status to inactive, and triggers a user.deactivated event. This event is consumed by a user_suspension_service that:

When a user accesses a service that requires additional permissions, the IdP presents a consent screen. The flow is initiated via an Authorization Code grant.

The client application sends a request:

GET /oauth/authorize?
  client_id=app-xyz
  &redirect_uri=https://app.acme.com/callback
  &response_type=code
  &scope=read:files write:files profile
  &state=abc123
  &prompt=consent
HTTP/1.1
Host: idp.acme.com

The IdP responds with a form that lists requested scopes and asks the user to approve:

> You’re signing in to App XYZ. Grant access to: > > - Read your files > - Write to your files > - View your profile > > [ ] Remember this decision for 30 days > [Submit]

After submission, the IdP returns a code via redirect_uri:

GET /callback?code=auth-code-456&state=abc123 HTTP/1.1
Host: app.acme.com

The client exchanges the code for an access token:

POST /oauth/token HTTP/1.1
Host: idp.acme.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=auth-code-456
&redirect_uri=https://app.acme.com/callback
&client_id=app-xyz
&client_secret=secret-abc

The response:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx.xxxxx",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "rt-789",
  "scope": "read:files write:files profile",
  "issued_at": 1710585600
}

The client uses this token to call the API:

GET /api/v1/files HTTP/1.1
Host: app.acme.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx.xxxxx

The IdP must track which users granted which scopes. This is stored in a user_scopes table:

user_idclient_idscopesgranted_atexpires_atremember
u-1234app-xyzread:files,write:files2024-03-152024-06-15true

When a user logs in again, the IdP checks if they previously granted consent for app-xyz. If remember = true, it skips the consent screen.

A common failure is scope drift: the client adds a new scope read:reports, but the IdP does not update its list of supported scopes. Users get 400 Bad Request when requesting read:reports, but the scope field in the token is read:files write:files profile, missing the new one.

To handle this, the client must dynamically discover the supported scopes via the .well-known/openid-configuration endpoint:

{
  "authorization_endpoint": "https://idp.acme.com/oauth/authorize",
  "token_endpoint": "https://idp.acme.com/oauth/token",
  "scopes_supported": [
    "openid",
    "profile",
    "email",
    "read:files",
    "write:files",
    "read:reports"
  ],
  "claims_supported": [
    "sub",
    "email",
    "name",
    "department"
  ]
}

The client checks scopes_supported and includes only valid scopes in the scope parameter. If a scope is invalid, the IdP returns:

{
  "error": "invalid_scope",
  "error_description": "The requested scope 'read:reports' is not supported by this authorization server.",
  "supported_scopes": [
    "openid",
    "profile",
    "email",
    "read:files",
    "write:files"
  ]
}

Federated Identity and Cross-System Access

Single Sign-On via SAML 2.0

When a user accesses a service, they may be redirected to the IdP for authentication. The IdP uses SAML 2.0 to exchange identity and authentication information.

The service sends an AuthnRequest as a POST to the IdP:

<samlp:AuthnRequest
  xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
  ID="req-1234"
  Version="2.0"
  IssueInstant="2024-03-15T12:00:00Z"
  Destination="https://idp.acme.com/saml/SSO"
  ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST">
  <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://app.acme.com</saml:Issuer>
  <samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" AllowCreate="true" />
</samlp:AuthnRequest>

The IdP authenticates the user and returns a SAMLResponse:

<samlp:Response
  xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
  ID="resp-5678"
  Version="2.0"
  IssueInstant="2024-03-15T12:00:30Z"
  Destination="https://app.acme.com/saml/acs"
  InResponseTo="req-1234">
  <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://idp.acme.com</saml:Issuer>
  <samlp:Status>
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
  </samlp:Status>
  <saml:Assertion
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
    ID="assertion-9012"
    IssueInstant="2024-03-15T12:00:30Z"
    Version="2.0">
    <saml:Issuer>https://idp.acme.com</saml:Issuer>
    <saml:Subject>
      <saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">u-1234</saml:NameID>
      <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <saml:SubjectConfirmationData
          InResponseTo="req-1234"
          NotOnOrAfter="2024-03-15T12:05:30Z"
          Recipient="https://app.acme.com/saml/acs" />
      </saml:SubjectConfirmation>
    </saml:Subject>
    <saml:Conditions NotBefore="2024-03-15T12:00:30Z" NotOnOrAfter="2024-03-15T12:05:30Z">
      <saml:AudienceRestriction>
        <saml:Audience>https://app.acme.com</saml:Audience>
      </saml:AudienceRestriction>
    </saml:Conditions>
    <saml:AttributeStatement>
      <saml:Attribute Name="email" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
        <saml:AttributeValue>[email protected]</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="department" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
        <saml:AttributeValue>engineering</saml:AttributeValue>
      </saml:Attribute>
    </saml:AttributeStatement>
  </saml:Assertion>
</samlp:Response>

The service validates the response:

A common silent failure is clock skew: if the IdP and service have different clocks, a NotOnOrAfter time may be invalid. The service must allow a buffer (e.g., 5 minutes) and log clock_skew_detected events.

Role-Based Access Control with Dynamic Policies

Hierarchical Roles and Nested Permissions

Roles are assigned to users via user_roles table:

user_idrole_ideffective_fromeffective_togranted_by
u-1234r-engineer2024-01-012025-01-01[email protected]

Each role has associated permissions:

{
  "role_id": "r-engineer",
  "name": "Engineer",
  "permissions": [
    "files:read",
    "files:write",
    "projects:edit",
    "users:manage"
  ],
  "scopes": [
    "read:files",
    "write:files"
  ],
  "description": "Full access to code and project systems"
}

An API endpoint /auth/permissions returns a user’s effective permissions:

GET /auth/permissions?user_id=u-1234 HTTP/1.1
Host: auth.acme.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx.xxxxx

Response:

{
  "user_id": "u-1234",
  "effective_permissions": [
    "files:read",
    "files:write",
    "projects:edit",
    "users:manage"
  ],
  "granted_by": [
    "role:r-engineer",
    "assignment:u-1234"
  ],
  "effective_from": "2024-01-01",
  "effective_to": "2025-01-01"
}

A key failure mode is role inheritance: a r-lead role includes r-engineer, but the auth-service does not recursively apply permissions. The service must query the role_hierarchy table and merge permissions.

When a user’s role changes, the auth-service must:

Token-Based Authentication and Session Management

Refreshing Access Tokens and Handling Token Expiry

Access tokens are short-lived (e.g., 1 hour). When a client receives a token, it must monitor its expiry and refresh it.

The auth-service returns a token with expires_in and refresh_token:

{
  "access_token": "a-1234",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "rt-5678"
}

The client stores the token and checks its expiry every 5 minutes. When expires_in < 300 seconds, it initiates a refresh:

POST /oauth/token HTTP/1.1
Host: idp.acme.com
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=rt-5678
&client_id=app-xyz
&client_secret=secret-abc

The IdP returns:

{
  "access_token": "a-9999",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "rt-1111",
  "scope": "read:files write:files profile"
}

A common failure is token invalidation: when a user logs out from one service, the IdP must invalidate the access_token and refresh_token. The service must check Authorization: Bearer <token> against a token_revocation table and return 401 Unauthorized if the token is invalid.

The IdP must also handle token rotation: when a refresh token is used, it generates a new refresh token and invalidates the old one. This prevents replay and enables seamless session continuation.

When a client fails to refresh in time, the user sees a “token expired” error:

{
  "error": "token_expired",
  "error_description": "The access token is no longer valid.",
  "error_uri": "https://docs.acme.com/errors/token-expired"
}

The client must retry the request with a new token, or redirect to the IdP for re-authentication.

This page was rewritten on 16 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.