# Identity Access Management

> Set up and manage SSO, SCIM provisioning, and MFA for startup teams using Google Workspace, Okta, or Azure AD. Use when centralizing authentication, onboarding SSO, or meeting compliance requirements.

- Skill: `gabrielmoreira/identity-access-management` (Agent Skill)
- Install (CLI): `npx skillmds@latest add gabrielmoreira/identity-access-management`
- Raw SKILL.md: https://api.skillmd.com/api/skills/gabrielmoreira/identity-access-management/raw
- Safety review: pending (external: skill-scanner PASS, skillspector CAUTION)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- License: MIT
- Author: gabrielmoreira (https://skillmd.com/u/gabrielmoreira)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/gabrielmoreira/identity-access-management

---


# Identity & Access Management for Startups

Centralized identity management is not optional once your team exceeds a handful of people. This skill covers practical, production-ready configurations for SSO, SCIM provisioning, MFA enforcement, and access governance using the three most common identity providers for startups: Google Workspace, Okta, and Azure AD (Entra ID).

---

## 1. When to Use This Skill

Reach for this skill when:

- **First SSO setup** -- You are moving from individual app logins to centralized authentication.
- **Compliance audit preparation** -- SOC 2, ISO 27001, or HIPAA requires documented access controls, MFA enforcement, and audit logs.
- **Team growth inflection** -- You are crossing 15-20 employees and manual onboarding/offboarding is becoming error-prone.
- **Vendor security questionnaires** -- Customers are asking about your identity posture and you need to demonstrate controls.
- **Incident response** -- You need to revoke access quickly across all systems for a departing or compromised user.

Signs you are overdue:

- Shared passwords in a spreadsheet or chat channel.
- No central audit log of who accessed what and when.
- Offboarding takes more than one business day.
- Developers have standing admin access to production.

---

## 2. Google Workspace as Identity Provider

Google Workspace is the most common starting IdP for startups. Combined with the GAM CLI tool, it provides powerful automation.

### Install GAM (Google Apps Manager)

```bash
# Install GAM on Linux/macOS
bash <(curl -s -S -L https://gam-shortn.appspot.com/gam-install)

# Authorize GAM with your Workspace domain
gam oauth create

# Verify connection
gam info domain
```

### Create Organizational Units

Organizational units (OUs) control policy inheritance and app access.

```bash
# Create OUs for team structure
gam create org "Engineering"
gam create org "Engineering/Backend"
gam create org "Engineering/Frontend"
gam create org "Operations"
gam create org "Operations/IT"
gam create org "Finance"
gam create org "Contractors"

# Move a user into an OU
gam update user alice@company.com org "Engineering/Backend"

# List all OUs
gam print orgs
```

### Configure a SAML App in Google Workspace

```bash
# Export the Google IdP metadata (download from Admin Console or use GAM)
# Admin Console: Apps > Web and mobile apps > Add app > Search for app > Download IdP metadata

# For a custom SAML app, you need:
# 1. ACS URL (from the service provider)
# 2. Entity ID (from the service provider)
# 3. Name ID format (usually EMAIL)

# Example: Add a custom SAML app via Admin Console API
gam create samlapp "Internal Dashboard" \
  acs_url "https://dashboard.company.com/saml/acs" \
  entity_id "https://dashboard.company.com" \
  name_id_format "EMAIL" \
  name_id "user.primaryEmail"

# Assign the app to an OU
gam update samlapp "Internal Dashboard" org "Engineering" enabled on

# Verify SAML app status
gam print samlappinfo "Internal Dashboard"
```

### SCIM Provisioning with Google Workspace

```bash
# Enable auto-provisioning for supported apps
# Google Workspace supports automatic user provisioning for apps like:
# Slack, Zoom, Box, Dropbox, Asana, GitHub Enterprise

# List provisioned apps
gam print tokens

# Force sync provisioning for an app
gam sync samlapp "Slack" users

# Bulk create users from CSV
# users.csv format: firstname,lastname,email,org,password
gam csv users.csv gam create user ~email \
  firstname ~firstname lastname ~lastname \
  password ~password org ~org \
  changepassword on
```

### Enforce MFA at the Workspace Level

```bash
# Enforce 2-step verification for the entire domain
gam update org "/" 2sv enforced

# Enforce 2SV for a specific OU
gam update org "Engineering" 2sv enforced

# Set enforcement date (give users time to enroll)
gam update org "/" 2sv enforced enforceddate 2026-04-15

# Check 2SV enrollment status for all users
gam print users fields isEnforcedIn2Sv,isEnrolledIn2Sv

# Find users who have NOT enrolled in 2SV
gam print users query "isEnrolledIn2Sv=false" fields primaryEmail,name
```

---

## 3. Okta Setup

Okta offers a free tier for startups (Okta for Startups program -- up to 100 users) making it an excellent choice for teams that need a dedicated IdP.

### Initial Okta Configuration via API

```bash
# Set your Okta domain and API token
export OKTA_ORG_URL="https://company.okta.com"
export OKTA_API_TOKEN="your-api-token"

# Verify connectivity
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  "${OKTA_ORG_URL}/api/v1/org" | jq '.companyName'

# Create a user
curl -s -X POST \
  -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  -H "Content-Type: application/json" \
  "${OKTA_ORG_URL}/api/v1/users?activate=true" \
  -d '{
    "profile": {
      "firstName": "Alice",
      "lastName": "Engineer",
      "email": "alice@company.com",
      "login": "alice@company.com"
    },
    "credentials": {
      "password": { "value": "TempP@ss123!" }
    }
  }' | jq '.id'
```

### Create Groups for RBAC

```bash
# Create groups
for group in "Engineering" "Operations" "Finance" "Contractors" "AdminAccess"; do
  curl -s -X POST \
    -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
    -H "Content-Type: application/json" \
    "${OKTA_ORG_URL}/api/v1/groups" \
    -d "{\"profile\": {\"name\": \"${group}\", \"description\": \"${group} team group\"}}" \
    | jq '{id: .id, name: .profile.name}'
done

# Add user to group
USER_ID="00u1abc123"
GROUP_ID="00g1def456"
curl -s -X PUT \
  -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  "${OKTA_ORG_URL}/api/v1/groups/${GROUP_ID}/users/${USER_ID}"
```

### Add a SAML Application in Okta

```bash
# Create a SAML 2.0 application
curl -s -X POST \
  -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  -H "Content-Type: application/json" \
  "${OKTA_ORG_URL}/api/v1/apps" \
  -d '{
    "name": "custom_saml_app",
    "label": "Internal Dashboard",
    "signOnMode": "SAML_2_0",
    "settings": {
      "signOn": {
        "defaultRelayState": "",
        "ssoAcsUrl": "https://dashboard.company.com/saml/acs",
        "audience": "https://dashboard.company.com",
        "recipient": "https://dashboard.company.com/saml/acs",
        "destination": "https://dashboard.company.com/saml/acs",
        "subjectNameIdFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
        "attributeStatements": [
          {
            "type": "EXPRESSION",
            "name": "email",
            "namespace": "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
            "values": ["user.email"]
          },
          {
            "type": "EXPRESSION",
            "name": "groups",
            "namespace": "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
            "values": ["getFilteredGroups({\"00g1def456\"}, \"group.name\", 50)"]
          }
        ]
      }
    }
  }' | jq '{id: .id, label: .label, status: .status}'

# Assign group to application
APP_ID="0oa1xyz789"
curl -s -X PUT \
  -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  -H "Content-Type: application/json" \
  "${OKTA_ORG_URL}/api/v1/apps/${APP_ID}/groups/${GROUP_ID}"
```

### Okta MFA Policy

```bash
# Create an MFA enrollment policy requiring WebAuthn + TOTP
curl -s -X POST \
  -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  -H "Content-Type: application/json" \
  "${OKTA_ORG_URL}/api/v1/policies" \
  -d '{
    "type": "MFA_ENROLL",
    "name": "Require Strong MFA",
    "status": "ACTIVE",
    "settings": {
      "factors": {
        "webauthn": { "enroll": { "self": "REQUIRED" } },
        "google_otp": { "enroll": { "self": "OPTIONAL" } },
        "okta_email": { "enroll": { "self": "NOT_ALLOWED" } },
        "okta_sms": { "enroll": { "self": "NOT_ALLOWED" } }
      }
    }
  }' | jq '{id: .id, name: .name, status: .status}'
```

---

## 4. Azure AD / Entra ID

Azure AD (now Microsoft Entra ID) is common at startups using Microsoft 365 or Azure cloud.

### Azure CLI Setup

```bash
# Install Azure CLI and sign in
az login

# Set the default tenant
az account set --subscription "your-subscription-id"

# Verify tenant
az ad signed-in-user show --query '{name:displayName, email:userPrincipalName}'
```

### Create Users and Groups

```bash
# Create a user
az ad user create \
  --display-name "Alice Engineer" \
  --user-principal-name "alice@company.onmicrosoft.com" \
  --password "TempP@ss123!" \
  --force-change-password-next-sign-in true

# Create security groups
for group in "SG-Engineering" "SG-Operations" "SG-Finance" "SG-Admins"; do
  az ad group create --display-name "$group" --mail-nickname "$group"
done

# Add user to group
USER_OID=$(az ad user show --id "alice@company.onmicrosoft.com" --query id -o tsv)
GROUP_OID=$(az ad group show --group "SG-Engineering" --query id -o tsv)
az ad group member add --group "$GROUP_OID" --member-id "$USER_OID"

# List group members
az ad group member list --group "SG-Engineering" --query '[].{name:displayName, email:userPrincipalName}' -o table
```

### Conditional Access Policies via Graph API

```bash
# Require MFA for all users accessing cloud apps
# Uses Microsoft Graph API
ACCESS_TOKEN=$(az account get-access-token --resource https://graph.microsoft.com --query accessToken -o tsv)

curl -s -X POST \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" \
  -d '{
    "displayName": "Require MFA for all users",
    "state": "enabledForReportingButNotEnforced",
    "conditions": {
      "users": {
        "includeUsers": ["All"],
        "excludeGroups": ["'${BREAKGLASS_GROUP_OID}'"]
      },
      "applications": {
        "includeApplications": ["All"]
      }
    },
    "grantControls": {
      "operator": "OR",
      "builtInControls": ["mfa"]
    }
  }'

# Block legacy authentication (critical for security)
curl -s -X POST \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" \
  -d '{
    "displayName": "Block legacy authentication",
    "state": "enabled",
    "conditions": {
      "users": { "includeUsers": ["All"] },
      "applications": { "includeApplications": ["All"] },
      "clientAppTypes": ["exchangeActiveSync", "other"]
    },
    "grantControls": {
      "operator": "OR",
      "builtInControls": ["block"]
    }
  }'
```

---

## 5. SSO Integration Patterns

### SAML vs OIDC Decision Guide

| Factor | SAML 2.0 | OIDC / OAuth 2.0 |
|---|---|---|
| Best for | Enterprise SaaS apps | SPAs, mobile apps, APIs |
| Token format | XML assertions | JWT tokens |
| Setup complexity | Higher (certificates, metadata XML) | Lower (client ID + secret) |
| Logout | Inconsistent (SLO is poorly supported) | Token expiry + revocation |
| Use when | App only supports SAML | You have a choice, or need API auth |

**Rule of thumb**: If the SaaS vendor supports OIDC, prefer it. If they only support SAML, use SAML. Never use LDAP-over-internet.

### Integrating Common SaaS Apps

#### Slack Enterprise SSO

```bash
# Okta OIDC integration for Slack
# 1. In Okta: Applications > Browse App Catalog > Slack
# 2. Configure with your Slack workspace URL
# 3. Enable SCIM provisioning

# Verify Slack SCIM connection
curl -s -H "Authorization: Bearer ${SLACK_SCIM_TOKEN}" \
  "https://api.slack.com/scim/v2/Users?count=5" | jq '.Resources[].userName'
```

#### GitHub Organization SSO

```bash
# Configure SAML for GitHub Org (requires GitHub Enterprise Cloud)
# 1. GitHub Org Settings > Authentication security > Enable SAML
# 2. Provide IdP SSO URL, IdP issuer, public certificate from your IdP

# Use GitHub CLI to verify SSO status
gh api orgs/company/credential-authorizations --paginate \
  | jq '.[] | {login: .login, credential_type: .credential_type, authorized_at: .authorized_credential_note}'

# Require SAML SSO for all org members
gh api -X PATCH orgs/company \
  -f saml_enforced=true
```

#### AWS SSO (IAM Identity Center)

```bash
# Configure AWS IAM Identity Center with external IdP
aws sso-admin list-instances --query 'Instances[0].InstanceArn' --output text

INSTANCE_ARN="arn:aws:sso:::instance/ssoins-1234567890"
IDENTITY_STORE_ID="d-1234567890"

# Create a permission set
aws sso-admin create-permission-set \
  --instance-arn "$INSTANCE_ARN" \
  --name "DeveloperAccess" \
  --description "Read-only + deploy access for engineers" \
  --session-duration "PT8H"

# Attach AWS managed policy to permission set
PERMISSION_SET_ARN="arn:aws:sso:::permissionSet/ssoins-1234567890/ps-abc123"
aws sso-admin attach-managed-policy-to-permission-set \
  --instance-arn "$INSTANCE_ARN" \
  --permission-set-arn "$PERMISSION_SET_ARN" \
  --managed-policy-arn "arn:aws:iam::aws:policy/ReadOnlyAccess"

# Assign group to AWS account with permission set
aws sso-admin create-account-assignment \
  --instance-arn "$INSTANCE_ARN" \
  --target-id "123456789012" \
  --target-type AWS_ACCOUNT \
  --permission-set-arn "$PERMISSION_SET_ARN" \
  --principal-type GROUP \
  --principal-id "a1b2c3d4-5678-90ab-cdef-GROUP001"
```

---

## 6. SCIM Provisioning

SCIM (System for Cross-domain Identity Management) automates user lifecycle across SaaS apps.

### SCIM API Examples

```bash
# Standard SCIM 2.0 endpoints (most IdPs and SaaS apps follow this)
SCIM_BASE="https://app.example.com/scim/v2"
SCIM_TOKEN="your-scim-bearer-token"

# List users
curl -s -H "Authorization: Bearer ${SCIM_TOKEN}" \
  "${SCIM_BASE}/Users?count=10&startIndex=1" | jq '.Resources[] | {id, userName, active}'

# Create a user via SCIM
curl -s -X POST \
  -H "Authorization: Bearer ${SCIM_TOKEN}" \
  -H "Content-Type: application/scim+json" \
  "${SCIM_BASE}/Users" \
  -d '{
    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
    "userName": "alice@company.com",
    "name": { "givenName": "Alice", "familyName": "Engineer" },
    "emails": [{ "primary": true, "value": "alice@company.com", "type": "work" }],
    "active": true,
    "groups": []
  }' | jq '{id, userName, active}'

# Deactivate a user via SCIM (PATCH is the standard for partial updates)
USER_SCIM_ID="abc-123-def"
curl -s -X PATCH \
  -H "Authorization: Bearer ${SCIM_TOKEN}" \
  -H "Content-Type: application/scim+json" \
  "${SCIM_BASE}/Users/${USER_SCIM_ID}" \
  -d '{
    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
    "Operations": [{ "op": "replace", "value": { "active": false } }]
  }' | jq '{id, userName, active}'

# Delete a user permanently via SCIM
curl -s -X DELETE \
  -H "Authorization: Bearer ${SCIM_TOKEN}" \
  "${SCIM_BASE}/Users/${USER_SCIM_ID}"
```

### SCIM Group Management

```bash
# Create a group via SCIM
curl -s -X POST \
  -H "Authorization: Bearer ${SCIM_TOKEN}" \
  -H "Content-Type: application/scim+json" \
  "${SCIM_BASE}/Groups" \
  -d '{
    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
    "displayName": "Engineering",
    "members": [
      { "value": "user-id-001", "display": "alice@company.com" },
      { "value": "user-id-002", "display": "bob@company.com" }
    ]
  }' | jq '{id, displayName}'

# Add a member to an existing group
GROUP_SCIM_ID="grp-456"
curl -s -X PATCH \
  -H "Authorization: Bearer ${SCIM_TOKEN}" \
  -H "Content-Type: application/scim+json" \
  "${SCIM_BASE}/Groups/${GROUP_SCIM_ID}" \
  -d '{
    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
    "Operations": [{
      "op": "add",
      "path": "members",
      "value": [{ "value": "user-id-003" }]
    }]
  }'
```

---

## 7. MFA Enforcement

### WebAuthn / Passkeys (Strongest)

WebAuthn (FIDO2) hardware keys and passkeys are phishing-resistant and should be the primary MFA factor.

```bash
# Okta: Enforce WebAuthn as primary factor
curl -s -X PUT \
  -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  -H "Content-Type: application/json" \
  "${OKTA_ORG_URL}/api/v1/org/factors/webauthn" \
  -d '{ "status": "ACTIVE" }'

# Google Workspace: Enforce security keys only (disable SMS/voice)
gam update org "/" 2sv enforced allowedmethods security_key

# Azure AD: Require phishing-resistant MFA via conditional access
# (use the Graph API conditional access endpoint with authenticationStrengths)
curl -s -X POST \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" \
  -d '{
    "displayName": "Require phishing-resistant MFA for admins",
    "state": "enabled",
    "conditions": {
      "users": { "includeRoles": ["62e90394-69f5-4237-9190-012177145e10"] },
      "applications": { "includeApplications": ["All"] }
    },
    "grantControls": {
      "operator": "OR",
      "authenticationStrength": {
        "id": "00000000-0000-0000-0000-000000000004"
      }
    }
  }'
```

### TOTP Backup Configuration

```bash
# Generate backup codes for users (Okta)
USER_ID="00u1abc123"
curl -s -X POST \
  -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  "${OKTA_ORG_URL}/api/v1/users/${USER_ID}/factors" \
  -d '{
    "factorType": "token:software:totp",
    "provider": "GOOGLE"
  }' | jq '{id: .id, status: .status}'
```

### MFA Bypass Procedure (Emergency)

```bash
# Okta: Reset MFA for a locked-out user
USER_ID="00u1abc123"
# List enrolled factors
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  "${OKTA_ORG_URL}/api/v1/users/${USER_ID}/factors" | jq '.[].factorType'

# Delete a specific factor to allow re-enrollment
FACTOR_ID="fct1abc123"
curl -s -X DELETE \
  -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  "${OKTA_ORG_URL}/api/v1/users/${USER_ID}/factors/${FACTOR_ID}"

# Google Workspace: Generate backup verification codes
gam user alice@company.com update backupcodes

# Azure AD: Require re-registration of MFA methods
az rest --method DELETE \
  --url "https://graph.microsoft.com/v1.0/users/${USER_OID}/authentication/phoneMethods/3179e48a-750b-4051-897c-87b9720928f7"
```

---

## 8. Role-Based Access Control

### Group-Based Access Patterns

Map every application permission to a group, never to an individual user.

```bash
# Naming convention: APP-ROLE
# Examples:
#   aws-developer       -> AWS ReadOnly + deploy
#   aws-admin           -> AWS AdministratorAccess
#   github-engineer     -> GitHub write access
#   github-admin        -> GitHub admin access
#   slack-member        -> Slack standard member
#   pagerduty-oncall    -> PagerDuty responder role

# Okta: Create group rules for automatic assignment based on department
curl -s -X POST \
  -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  -H "Content-Type: application/json" \
  "${OKTA_ORG_URL}/api/v1/groups/rules" \
  -d '{
    "type": "group_rule",
    "name": "Auto-assign engineers to GitHub",
    "conditions": {
      "expression": {
        "value": "user.department == \"Engineering\"",
        "type": "urn:okta:expression:1.0"
      }
    },
    "actions": {
      "assignUserToGroups": { "groupIds": ["GITHUB_ENGINEERS_GROUP_ID"] }
    }
  }'
```

### Just-in-Time (JIT) Access

```bash
# AWS: Grant temporary elevated access using STS assume-role
# The user assumes a role that expires after a set duration
aws sts assume-role \
  --role-arn "arn:aws:iam::123456789012:role/EmergencyAdmin" \
  --role-session-name "alice-incident-2026-03-24" \
  --duration-seconds 3600 \
  | jq '{AccessKeyId: .Credentials.AccessKeyId, Expiration: .Credentials.Expiration}'

# Okta: Create a time-limited group membership (via API scheduled task)
# Add user to admin group
curl -s -X PUT \
  -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  "${OKTA_ORG_URL}/api/v1/groups/${ADMIN_GROUP_ID}/users/${USER_ID}"

# Schedule removal after 4 hours (use a cron job or automation tool)
echo "0 */4 * * * curl -s -X DELETE -H 'Authorization: SSWS ${OKTA_API_TOKEN}' \
  '${OKTA_ORG_URL}/api/v1/groups/${ADMIN_GROUP_ID}/users/${USER_ID}'" | crontab -
```

### Break-Glass Accounts

```bash
# Create break-glass accounts that bypass SSO/MFA for emergency access
# These accounts must be:
# 1. Excluded from conditional access / MFA policies
# 2. Protected with extremely long passwords stored in a physical safe
# 3. Monitored with alerts on any usage

# Azure AD: Create break-glass account
az ad user create \
  --display-name "Break Glass 1" \
  --user-principal-name "breakglass1@company.onmicrosoft.com" \
  --password "$(openssl rand -base64 48)" \
  --force-change-password-next-sign-in false

# Assign Global Administrator role
az ad group member add --group "SG-BreakGlass" --member-id "$BREAKGLASS_OID"

# Set up alert on break-glass sign-in (Azure Monitor)
az monitor activity-log alert create \
  --name "BreakGlass-SignIn-Alert" \
  --resource-group "security-rg" \
  --condition category=Administrative and caller=breakglass1@company.onmicrosoft.com \
  --action-group "/subscriptions/SUB_ID/resourceGroups/security-rg/providers/microsoft.insights/actionGroups/SecurityTeam"
```

---

## 9. Audit & Compliance

### Login Audit Logs

```bash
# Google Workspace: Pull login audit logs
gam report login user all start "2026-03-01" end "2026-03-24" \
  fields "actorEmail,ipAddress,loginType,isSecondFactor,isSuspicious"

# Okta: Query system log for authentication events
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  "${OKTA_ORG_URL}/api/v1/logs?filter=eventType+eq+\"user.session.start\"&since=2026-03-01T00:00:00Z&limit=100" \
  | jq '.[] | {actor: .actor.displayName, time: .published, outcome: .outcome.result, ip: .client.ipAddress}'

# Azure AD: Pull sign-in logs via Graph API
curl -s -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  "https://graph.microsoft.com/v1.0/auditLogs/signIns?\$filter=createdDateTime ge 2026-03-01T00:00:00Z&\$top=50" \
  | jq '.value[] | {user: .userDisplayName, app: .appDisplayName, status: .status.errorCode, ip: .ipAddress, mfa: .mfaDetail}'
```

### Access Reviews

```bash
# List all users and their group memberships for quarterly access review
# Google Workspace
gam print group-members fields email,role > /tmp/access-review-groups.csv

# Okta: Export all users with their app assignments
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  "${OKTA_ORG_URL}/api/v1/users?limit=200" \
  | jq -r '.[] | [.profile.email, .status, .lastLogin] | @csv' > /tmp/okta-users.csv

# For each user, list their app assignments
while IFS= read -r user_id; do
  curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
    "${OKTA_ORG_URL}/api/v1/users/${user_id}/appLinks" \
    | jq -r '.[] | [.label, .linkUrl] | @csv'
done < /tmp/okta-user-ids.txt > /tmp/okta-access-review.csv

# Azure AD: List role assignments
az role assignment list --all --query '[].{principal:principalName, role:roleDefinitionName, scope:scope}' -o table
```

### Compliance Reporting

```bash
# Count of users with/without MFA enrolled
# Google Workspace
echo "=== MFA Enrollment Report ==="
echo "Enrolled:"
gam print users fields isEnrolledIn2Sv | grep -c True
echo "Not enrolled:"
gam print users fields isEnrolledIn2Sv | grep -c False

# Okta: Users without any MFA factor
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  "${OKTA_ORG_URL}/api/v1/users?filter=status+eq+\"ACTIVE\"&limit=200" \
  | jq '[.[] | select(.credentials.provider.type != "SOCIAL") | .id] | length'

# Check for stale accounts (no login in 90 days)
NINETY_DAYS_AGO=$(date -d "-90 days" +%Y-%m-%dT00:00:00Z 2>/dev/null || date -v-90d +%Y-%m-%dT00:00:00Z)
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  "${OKTA_ORG_URL}/api/v1/users?filter=lastLogin+lt+\"${NINETY_DAYS_AGO}\"&limit=200" \
  | jq '.[] | {email: .profile.email, lastLogin: .lastLogin}'
```

---

## 10. Offboarding

### Account Deactivation Checklist

Run this sequence when an employee departs. Order matters -- revoke sessions first, then deactivate.

```bash
DEPARTING_USER="alice@company.com"

# Step 1: Revoke all active sessions immediately
# Okta
USER_ID=$(curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  "${OKTA_ORG_URL}/api/v1/users/${DEPARTING_USER}" | jq -r '.id')

curl -s -X DELETE \
  -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  "${OKTA_ORG_URL}/api/v1/users/${USER_ID}/sessions"

# Google Workspace: Revoke tokens and sign out
gam user "${DEPARTING_USER}" signout
gam user "${DEPARTING_USER}" deprovision

# Azure AD: Revoke all refresh tokens
az ad user update --id "${DEPARTING_USER}" --account-enabled false
az rest --method POST \
  --url "https://graph.microsoft.com/v1.0/users/${DEPARTING_USER}/revokeSignInSessions"

# Step 2: Deactivate the user account
# Okta
curl -s -X POST \
  -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  "${OKTA_ORG_URL}/api/v1/users/${USER_ID}/lifecycle/deactivate"

# Google Workspace
gam update user "${DEPARTING_USER}" suspended on

# Step 3: Transfer data ownership
# Google Workspace: Transfer Drive files
gam user "${DEPARTING_USER}" transfer drive manager@company.com

# Google Workspace: Transfer Calendar ownership
gam user "${DEPARTING_USER}" transfer calendar manager@company.com

# Step 4: Remove from all groups (prevents future provisioning)
# Okta
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  "${OKTA_ORG_URL}/api/v1/users/${USER_ID}/groups" \
  | jq -r '.[].id' | while read gid; do
    curl -s -X DELETE \
      -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
      "${OKTA_ORG_URL}/api/v1/groups/${gid}/users/${USER_ID}"
  done

# Step 5: Revoke app-specific tokens
# GitHub: Remove from org
gh api -X DELETE "orgs/company/members/${DEPARTING_USER}"

# Slack: Deactivate via SCIM
SLACK_USER_ID=$(curl -s -H "Authorization: Bearer ${SLACK_SCIM_TOKEN}" \
  "https://api.slack.com/scim/v2/Users?filter=userName+eq+\"${DEPARTING_USER}\"" \
  | jq -r '.Resources[0].id')

curl -s -X PATCH \
  -H "Authorization: Bearer ${SLACK_SCIM_TOKEN}" \
  -H "Content-Type: application/scim+json" \
  "https://api.slack.com/scim/v2/Users/${SLACK_USER_ID}" \
  -d '{"schemas":["urn:ietf:params:scim:api:messages:2.0:PatchOp"],"Operations":[{"op":"replace","value":{"active":false}}]}'

# AWS: Remove SSO access
aws sso-admin delete-account-assignment \
  --instance-arn "$INSTANCE_ARN" \
  --target-id "123456789012" \
  --target-type AWS_ACCOUNT \
  --permission-set-arn "$PERMISSION_SET_ARN" \
  --principal-type USER \
  --principal-id "$AWS_SSO_USER_ID"

# Step 6: Document and log
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | OFFBOARD | ${DEPARTING_USER} | all sessions revoked, account suspended, data transferred to manager@company.com" >> /var/log/offboarding-audit.log
```

### Post-Offboarding Verification

```bash
DEPARTING_USER="alice@company.com"

# Verify account is suspended/deactivated
echo "=== Offboarding Verification ==="

# Google Workspace
gam info user "${DEPARTING_USER}" fields suspended | grep -i "suspended: true" && echo "[OK] Google suspended" || echo "[FAIL] Google still active"

# Okta
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  "${OKTA_ORG_URL}/api/v1/users/${DEPARTING_USER}" \
  | jq -r '.status' | grep -q "DEPROVISIONED" && echo "[OK] Okta deprovisioned" || echo "[FAIL] Okta still active"

# GitHub
gh api "orgs/company/members/${DEPARTING_USER}" 2>&1 | grep -q "404" && echo "[OK] GitHub removed" || echo "[FAIL] GitHub still member"

# Check for any remaining active sessions in audit logs
echo "=== Checking for post-offboard activity ==="
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  "${OKTA_ORG_URL}/api/v1/logs?filter=actor.alternateId+eq+\"${DEPARTING_USER}\"&since=$(date -u +%Y-%m-%dT%H:%M:%SZ)&limit=10" \
  | jq '.[] | {time: .published, event: .eventType, outcome: .outcome.result}'
```

---

## Quick Reference

| Task | Google Workspace | Okta | Azure AD |
|---|---|---|---|
| Create user | `gam create user` | `POST /api/v1/users` | `az ad user create` |
| Suspend user | `gam update user suspended on` | `POST /lifecycle/deactivate` | `az ad user update --account-enabled false` |
| Enforce MFA | `gam update org 2sv enforced` | MFA enrollment policy | Conditional access policy |
| Revoke sessions | `gam user signout` | `DELETE /users/{id}/sessions` | `revokeSignInSessions` |
| Audit logins | `gam report login` | `GET /api/v1/logs` | `GET /auditLogs/signIns` |
| SCIM provision | Built-in for supported apps | App integration SCIM tab | Enterprise app provisioning |

