identity-access-management — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited identity-access-management (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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).
Reach for this skill when:
Signs you are overdue:
Google Workspace is the most common starting IdP for startups. Combined with the GAM CLI tool, it provides powerful automation.
# 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 domainOrganizational units (OUs) control policy inheritance and app access.
# 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 [email protected] org "Engineering/Backend"
# List all OUs
gam print orgs# 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"# 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 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,nameOkta 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.
# 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": "[email protected]",
"login": "[email protected]"
},
"credentials": {
"password": { "value": "TempP@ss123!" }
}
}' | jq '.id'# 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}"# 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}"# 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}'Azure AD (now Microsoft Entra ID) is common at startups using Microsoft 365 or Azure cloud.
# 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 a user
az ad user create \
--display-name "Alice Engineer" \
--user-principal-name "[email protected]" \
--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 "[email protected]" --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# 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"]
}
}'| 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.
#### Slack Enterprise SSO
# 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
# 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)
# 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"SCIM (System for Cross-domain Identity Management) automates user lifecycle across SaaS apps.
# 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": "[email protected]",
"name": { "givenName": "Alice", "familyName": "Engineer" },
"emails": [{ "primary": true, "value": "[email protected]", "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}"# 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": "[email protected]" },
{ "value": "user-id-002", "display": "[email protected]" }
]
}' | 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" }]
}]
}'WebAuthn (FIDO2) hardware keys and passkeys are phishing-resistant and should be the primary MFA factor.
# 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"
}
}
}'# 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}'# 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 [email protected] 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"Map every application permission to a group, never to an individual user.
# 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"] }
}
}'# 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 -# 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 "[email protected]" \
--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 [email protected] \
--action-group "/subscriptions/SUB_ID/resourceGroups/security-rg/providers/microsoft.insights/actionGroups/SecurityTeam"# 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}'# 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# 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}'Run this sequence when an employee departs. Order matters -- revoke sessions first, then deactivate.
DEPARTING_USER="[email protected]"
# 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 [email protected]
# Google Workspace: Transfer Calendar ownership
gam user "${DEPARTING_USER}" transfer calendar [email protected]
# 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 [email protected]" >> /var/log/offboarding-audit.logDEPARTING_USER="[email protected]"
# 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}'| 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 |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.