identity-access-management
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseIdentity & 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).
当团队规模超过几个人后,集中式身份管理就不再是可选项。本指南涵盖了针对创业公司最常用的三个身份提供商——Google Workspace、Okta和Azure AD(Entra ID)的SSO、SCIM配置、MFA强制实施以及访问治理的实用、可用于生产环境的配置方案。
1. When to Use This Skill
1. 适用场景
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.
在以下场景中可使用本指南:
- 首次SSO部署——正从单个应用独立登录转向集中式身份认证。
- 合规审计准备——SOC 2、ISO 27001或HIPAA要求具备可追溯的访问控制、MFA强制实施机制和审计日志。
- 团队规模扩张——员工数量达到15-20人,手动入职/离职流程开始频繁出错。
- 供应商安全问卷——客户询问你的身份安全状况,需要展示相关控制措施。
- 事件响应——需要快速撤销离职或账号泄露用户在所有系统中的访问权限。
以下信号表明你亟需实施集中式身份管理:
- 在电子表格或聊天频道中共享密码。
- 没有记录谁在何时访问了什么内容的集中审计日志。
- 离职流程耗时超过一个工作日。
- 开发人员长期拥有生产环境的管理员权限。
2. Google Workspace as Identity Provider
2. 以Google Workspace作为身份提供商
Google Workspace is the most common starting IdP for startups. Combined with the GAM CLI tool, it provides powerful automation.
Google Workspace是创业公司最常用的初始身份提供商。结合GAM CLI工具,它能提供强大的自动化能力。
Install GAM (Google Apps Manager)
安装GAM(Google Apps Manager)
bash
undefinedbash
undefinedInstall GAM on Linux/macOS
Install GAM on Linux/macOS
bash <(curl -s -S -L https://gam-shortn.appspot.com/gam-install)
bash <(curl -s -S -L https://gam-shortn.appspot.com/gam-install)
Authorize GAM with your Workspace domain
Authorize GAM with your Workspace domain
gam oauth create
gam oauth create
Verify connection
Verify connection
gam info domain
undefinedgam info domain
undefinedCreate Organizational Units
创建组织单元
Organizational units (OUs) control policy inheritance and app access.
bash
undefined组织单元(OU)用于控制策略继承和应用访问权限。
bash
undefinedCreate OUs for team structure
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"
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
Move a user into an OU
gam update user alice@company.com org "Engineering/Backend"
gam update user alice@company.com org "Engineering/Backend"
List all OUs
List all OUs
gam print orgs
undefinedgam print orgs
undefinedConfigure a SAML App in Google Workspace
在Google Workspace中配置SAML应用
bash
undefinedbash
undefinedExport the Google IdP metadata (download from Admin Console or use GAM)
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
Admin Console: Apps > Web and mobile apps > Add app > Search for app > Download IdP metadata
For a custom SAML app, you need:
For a custom SAML app, you need:
1. ACS URL (from the service provider)
1. ACS URL (from the service provider)
2. Entity ID (from the service provider)
2. Entity ID (from the service provider)
3. Name ID format (usually EMAIL)
3. Name ID format (usually EMAIL)
Example: Add a custom SAML app via Admin Console API
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"
acs_url "https://dashboard.company.com/saml/acs"
entity_id "https://dashboard.company.com"
name_id_format "EMAIL"
name_id "user.primaryEmail"
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"
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
Assign the app to an OU
gam update samlapp "Internal Dashboard" org "Engineering" enabled on
gam update samlapp "Internal Dashboard" org "Engineering" enabled on
Verify SAML app status
Verify SAML app status
gam print samlappinfo "Internal Dashboard"
undefinedgam print samlappinfo "Internal Dashboard"
undefinedSCIM Provisioning with Google Workspace
Google Workspace的SCIM配置
bash
undefinedbash
undefinedEnable auto-provisioning for supported apps
Enable auto-provisioning for supported apps
Google Workspace supports automatic user provisioning for apps like:
Google Workspace supports automatic user provisioning for apps like:
Slack, Zoom, Box, Dropbox, Asana, GitHub Enterprise
Slack, Zoom, Box, Dropbox, Asana, GitHub Enterprise
List provisioned apps
List provisioned apps
gam print tokens
gam print tokens
Force sync provisioning for an app
Force sync provisioning for an app
gam sync samlapp "Slack" users
gam sync samlapp "Slack" users
Bulk create users from CSV
Bulk create users from CSV
users.csv format: firstname,lastname,email,org,password
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
firstname ~firstname lastname ~lastname
password ~password org ~org
changepassword on
undefinedgam csv users.csv gam create user ~email
firstname ~firstname lastname ~lastname
password ~password org ~org
changepassword on
firstname ~firstname lastname ~lastname
password ~password org ~org
changepassword on
undefinedEnforce MFA at the Workspace Level
在Workspace层面强制实施MFA
bash
undefinedbash
undefinedEnforce 2-step verification for the entire domain
Enforce 2-step verification for the entire domain
gam update org "/" 2sv enforced
gam update org "/" 2sv enforced
Enforce 2SV for a specific OU
Enforce 2SV for a specific OU
gam update org "Engineering" 2sv enforced
gam update org "Engineering" 2sv enforced
Set enforcement date (give users time to enroll)
Set enforcement date (give users time to enroll)
gam update org "/" 2sv enforced enforceddate 2026-04-15
gam update org "/" 2sv enforced enforceddate 2026-04-15
Check 2SV enrollment status for all users
Check 2SV enrollment status for all users
gam print users fields isEnforcedIn2Sv,isEnrolledIn2Sv
gam print users fields isEnforcedIn2Sv,isEnrolledIn2Sv
Find users who have NOT enrolled in 2SV
Find users who have NOT enrolled in 2SV
gam print users query "isEnrolledIn2Sv=false" fields primaryEmail,name
---gam print users query "isEnrolledIn2Sv=false" fields primaryEmail,name
---3. Okta Setup
3. Okta配置
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.
Okta为创业公司提供免费套餐(Okta for Startups计划——最多支持100名用户),对于需要专用身份提供商的团队来说是绝佳选择。
Initial Okta Configuration via API
通过API进行初始Okta配置
bash
undefinedbash
undefinedSet your Okta domain and API token
Set your Okta domain and API token
export OKTA_ORG_URL="https://company.okta.com"
export OKTA_API_TOKEN="your-api-token"
export OKTA_ORG_URL="https://company.okta.com"
export OKTA_API_TOKEN="your-api-token"
Verify connectivity
Verify connectivity
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}"
"${OKTA_ORG_URL}/api/v1/org" | jq '.companyName'
"${OKTA_ORG_URL}/api/v1/org" | jq '.companyName'
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}"
"${OKTA_ORG_URL}/api/v1/org" | jq '.companyName'
"${OKTA_ORG_URL}/api/v1/org" | jq '.companyName'
Create a user
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'
-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'
undefinedcurl -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'
-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'
undefinedCreate Groups for RBAC
创建用于RBAC的群组
bash
undefinedbash
undefinedCreate groups
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
-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
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
-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
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}"
-H "Authorization: SSWS ${OKTA_API_TOKEN}"
"${OKTA_ORG_URL}/api/v1/groups/${GROUP_ID}/users/${USER_ID}"
undefinedUSER_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}"
-H "Authorization: SSWS ${OKTA_API_TOKEN}"
"${OKTA_ORG_URL}/api/v1/groups/${GROUP_ID}/users/${USER_ID}"
undefinedAdd a SAML Application in Okta
在Okta中添加SAML应用
bash
undefinedbash
undefinedCreate a SAML 2.0 application
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}'
-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}'
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}'
-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
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}"
-H "Authorization: SSWS ${OKTA_API_TOKEN}"
-H "Content-Type: application/json"
"${OKTA_ORG_URL}/api/v1/apps/${APP_ID}/groups/${GROUP_ID}"
undefinedAPP_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}"
-H "Authorization: SSWS ${OKTA_API_TOKEN}"
-H "Content-Type: application/json"
"${OKTA_ORG_URL}/api/v1/apps/${APP_ID}/groups/${GROUP_ID}"
undefinedOkta MFA Policy
Okta MFA策略
bash
undefinedbash
undefinedCreate an MFA enrollment policy requiring WebAuthn + TOTP
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}'
-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}'
---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}'
-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
4. Azure AD / Entra ID
Azure AD (now Microsoft Entra ID) is common at startups using Microsoft 365 or Azure cloud.
Azure AD(现名为Microsoft Entra ID)在使用Microsoft 365或Azure云的创业公司中较为常见。
Azure CLI Setup
Azure CLI配置
bash
undefinedbash
undefinedInstall Azure CLI and sign in
Install Azure CLI and sign in
az login
az login
Set the default tenant
Set the default tenant
az account set --subscription "your-subscription-id"
az account set --subscription "your-subscription-id"
Verify tenant
Verify tenant
az ad signed-in-user show --query '{name:displayName, email:userPrincipalName}'
undefinedaz ad signed-in-user show --query '{name:displayName, email:userPrincipalName}'
undefinedCreate Users and Groups
创建用户和群组
bash
undefinedbash
undefinedCreate a user
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
--display-name "Alice Engineer"
--user-principal-name "alice@company.onmicrosoft.com"
--password "TempP@ss123!"
--force-change-password-next-sign-in true
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
--display-name "Alice Engineer"
--user-principal-name "alice@company.onmicrosoft.com"
--password "TempP@ss123!"
--force-change-password-next-sign-in true
Create security groups
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
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
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"
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
List group members
az ad group member list --group "SG-Engineering" --query '[].{name:displayName, email:userPrincipalName}' -o table
undefinedaz ad group member list --group "SG-Engineering" --query '[].{name:displayName, email:userPrincipalName}' -o table
undefinedConditional Access Policies via Graph API
通过Graph API配置条件访问策略
bash
undefinedbash
undefinedRequire MFA for all users accessing cloud apps
Require MFA for all users accessing cloud apps
Uses Microsoft Graph API
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"] } }'
-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"] } }'
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"] } }'
-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)
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"] } }'
-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"] } }'
---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"] } }'
-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
5. SSO集成模式
SAML vs OIDC Decision Guide
SAML vs OIDC决策指南
| 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.
| 因素 | SAML 2.0 | OIDC / OAuth 2.0 |
|---|---|---|
| 最佳适用场景 | 企业级SaaS应用 | 单页应用、移动应用、API |
| 令牌格式 | XML断言 | JWT令牌 |
| 配置复杂度 | 较高(证书、元数据XML) | 较低(客户端ID+密钥) |
| 登出机制 | 不一致(SLO支持度差) | 令牌过期+撤销 |
| 使用时机 | 应用仅支持SAML | 有选择余地,或需要API认证 |
经验法则:如果SaaS供应商支持OIDC,优先选择OIDC;如果仅支持SAML,则使用SAML。绝对不要使用互联网环境下的LDAP。
Integrating Common SaaS Apps
常见SaaS应用集成
Slack Enterprise SSO
Slack Enterprise SSO
bash
undefinedbash
undefinedOkta OIDC integration for Slack
Okta OIDC integration for Slack
1. In Okta: Applications > Browse App Catalog > Slack
1. In Okta: Applications > Browse App Catalog > Slack
2. Configure with your Slack workspace URL
2. Configure with your Slack workspace URL
3. Enable SCIM provisioning
3. Enable SCIM provisioning
Verify Slack SCIM connection
Verify Slack SCIM connection
curl -s -H "Authorization: Bearer ${SLACK_SCIM_TOKEN}"
"https://api.slack.com/scim/v2/Users?count=5" | jq '.Resources[].userName'
"https://api.slack.com/scim/v2/Users?count=5" | jq '.Resources[].userName'
undefinedcurl -s -H "Authorization: Bearer ${SLACK_SCIM_TOKEN}"
"https://api.slack.com/scim/v2/Users?count=5" | jq '.Resources[].userName'
"https://api.slack.com/scim/v2/Users?count=5" | jq '.Resources[].userName'
undefinedGitHub Organization SSO
GitHub组织SSO
bash
undefinedbash
undefinedConfigure SAML for GitHub Org (requires GitHub Enterprise Cloud)
Configure SAML for GitHub Org (requires GitHub Enterprise Cloud)
1. GitHub Org Settings > Authentication security > Enable SAML
1. GitHub Org Settings > Authentication security > Enable SAML
2. Provide IdP SSO URL, IdP issuer, public certificate from your IdP
2. Provide IdP SSO URL, IdP issuer, public certificate from your IdP
Use GitHub CLI to verify SSO status
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}'
| jq '.[] | {login: .login, credential_type: .credential_type, authorized_at: .authorized_credential_note}'
gh api orgs/company/credential-authorizations --paginate
| jq '.[] | {login: .login, credential_type: .credential_type, authorized_at: .authorized_credential_note}'
| jq '.[] | {login: .login, credential_type: .credential_type, authorized_at: .authorized_credential_note}'
Require SAML SSO for all org members
Require SAML SSO for all org members
gh api -X PATCH orgs/company
-f saml_enforced=true
-f saml_enforced=true
undefinedgh api -X PATCH orgs/company
-f saml_enforced=true
-f saml_enforced=true
undefinedAWS SSO (IAM Identity Center)
AWS SSO(IAM Identity Center)
bash
undefinedbash
undefinedConfigure AWS IAM Identity Center with external IdP
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"
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
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"
--instance-arn "$INSTANCE_ARN"
--name "DeveloperAccess"
--description "Read-only + deploy access for engineers"
--session-duration "PT8H"
aws sso-admin create-permission-set
--instance-arn "$INSTANCE_ARN"
--name "DeveloperAccess"
--description "Read-only + deploy access for engineers"
--session-duration "PT8H"
--instance-arn "$INSTANCE_ARN"
--name "DeveloperAccess"
--description "Read-only + deploy access for engineers"
--session-duration "PT8H"
Attach AWS managed policy to permission set
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"
--instance-arn "$INSTANCE_ARN"
--permission-set-arn "$PERMISSION_SET_ARN"
--managed-policy-arn "arn:aws:iam::aws:policy/ReadOnlyAccess"
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"
--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
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"
--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"
---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"
--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
6. SCIM配置
SCIM (System for Cross-domain Identity Management) automates user lifecycle across SaaS apps.
SCIM(跨域身份管理系统)可自动化SaaS应用中的用户生命周期管理。
SCIM API Examples
SCIM API示例
bash
undefinedbash
undefinedStandard SCIM 2.0 endpoints (most IdPs and SaaS apps follow this)
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"
SCIM_BASE="https://app.example.com/scim/v2"
SCIM_TOKEN="your-scim-bearer-token"
List users
List users
curl -s -H "Authorization: Bearer ${SCIM_TOKEN}"
"${SCIM_BASE}/Users?count=10&startIndex=1" | jq '.Resources[] | {id, userName, active}'
"${SCIM_BASE}/Users?count=10&startIndex=1" | jq '.Resources[] | {id, userName, active}'
curl -s -H "Authorization: Bearer ${SCIM_TOKEN}"
"${SCIM_BASE}/Users?count=10&startIndex=1" | jq '.Resources[] | {id, userName, active}'
"${SCIM_BASE}/Users?count=10&startIndex=1" | jq '.Resources[] | {id, userName, active}'
Create a user via SCIM
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}'
-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}'
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}'
-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)
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}'
-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}'
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}'
-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
Delete a user permanently via SCIM
curl -s -X DELETE
-H "Authorization: Bearer ${SCIM_TOKEN}"
"${SCIM_BASE}/Users/${USER_SCIM_ID}"
-H "Authorization: Bearer ${SCIM_TOKEN}"
"${SCIM_BASE}/Users/${USER_SCIM_ID}"
undefinedcurl -s -X DELETE
-H "Authorization: Bearer ${SCIM_TOKEN}"
"${SCIM_BASE}/Users/${USER_SCIM_ID}"
-H "Authorization: Bearer ${SCIM_TOKEN}"
"${SCIM_BASE}/Users/${USER_SCIM_ID}"
undefinedSCIM Group Management
SCIM群组管理
bash
undefinedbash
undefinedCreate a group via SCIM
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}'
-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}'
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}'
-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
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" }] }] }'
-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" }] }] }'
---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" }] }] }'
-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
7. MFA强制实施
WebAuthn / Passkeys (Strongest)
WebAuthn / Passkeys(最安全)
WebAuthn (FIDO2) hardware keys and passkeys are phishing-resistant and should be the primary MFA factor.
bash
undefinedWebAuthn(FIDO2)硬件密钥和Passkeys具备防钓鱼能力,应作为主要的MFA验证方式。
bash
undefinedOkta: Enforce WebAuthn as primary 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" }'
-H "Authorization: SSWS ${OKTA_API_TOKEN}"
-H "Content-Type: application/json"
"${OKTA_ORG_URL}/api/v1/org/factors/webauthn"
-d '{ "status": "ACTIVE" }'
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" }'
-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)
Google Workspace: Enforce security keys only (disable SMS/voice)
gam update org "/" 2sv enforced allowedmethods security_key
gam update org "/" 2sv enforced allowedmethods security_key
Azure AD: Require phishing-resistant MFA via conditional access
Azure AD: Require phishing-resistant MFA via conditional access
(use the Graph API conditional access endpoint with authenticationStrengths)
(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" } } }'
-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" } } }'
undefinedcurl -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" } } }'
-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" } } }'
undefinedTOTP Backup Configuration
TOTP备份配置
bash
undefinedbash
undefinedGenerate backup codes for users (Okta)
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}'
-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}'
undefinedUSER_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}'
-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}'
undefinedMFA Bypass Procedure (Emergency)
MFA绕过流程(紧急情况)
bash
undefinedbash
undefinedOkta: Reset MFA for a locked-out user
Okta: Reset MFA for a locked-out user
USER_ID="00u1abc123"
USER_ID="00u1abc123"
List enrolled factors
List enrolled factors
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}"
"${OKTA_ORG_URL}/api/v1/users/${USER_ID}/factors" | jq '.[].factorType'
"${OKTA_ORG_URL}/api/v1/users/${USER_ID}/factors" | jq '.[].factorType'
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}"
"${OKTA_ORG_URL}/api/v1/users/${USER_ID}/factors" | jq '.[].factorType'
"${OKTA_ORG_URL}/api/v1/users/${USER_ID}/factors" | jq '.[].factorType'
Delete a specific factor to allow re-enrollment
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}"
-H "Authorization: SSWS ${OKTA_API_TOKEN}"
"${OKTA_ORG_URL}/api/v1/users/${USER_ID}/factors/${FACTOR_ID}"
FACTOR_ID="fct1abc123"
curl -s -X DELETE
-H "Authorization: SSWS ${OKTA_API_TOKEN}"
"${OKTA_ORG_URL}/api/v1/users/${USER_ID}/factors/${FACTOR_ID}"
-H "Authorization: SSWS ${OKTA_API_TOKEN}"
"${OKTA_ORG_URL}/api/v1/users/${USER_ID}/factors/${FACTOR_ID}"
Google Workspace: Generate backup verification codes
Google Workspace: Generate backup verification codes
gam user alice@company.com update backupcodes
gam user alice@company.com update backupcodes
Azure AD: Require re-registration of MFA methods
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"
--url "https://graph.microsoft.com/v1.0/users/${USER_OID}/authentication/phoneMethods/3179e48a-750b-4051-897c-87b9720928f7"
---az rest --method DELETE
--url "https://graph.microsoft.com/v1.0/users/${USER_OID}/authentication/phoneMethods/3179e48a-750b-4051-897c-87b9720928f7"
--url "https://graph.microsoft.com/v1.0/users/${USER_OID}/authentication/phoneMethods/3179e48a-750b-4051-897c-87b9720928f7"
---8. Role-Based Access Control
8. 基于角色的访问控制(RBAC)
Group-Based Access Patterns
基于群组的访问模式
Map every application permission to a group, never to an individual user.
bash
undefined将所有应用权限映射到群组,而非单个用户。
bash
undefinedNaming convention: APP-ROLE
Naming convention: APP-ROLE
Examples:
Examples:
aws-developer -> AWS ReadOnly + deploy
aws-developer -> AWS ReadOnly + deploy
aws-admin -> AWS AdministratorAccess
aws-admin -> AWS AdministratorAccess
github-engineer -> GitHub write access
github-engineer -> GitHub write access
github-admin -> GitHub admin access
github-admin -> GitHub admin access
slack-member -> Slack standard member
slack-member -> Slack standard member
pagerduty-oncall -> PagerDuty responder role
pagerduty-oncall -> PagerDuty responder role
Okta: Create group rules for automatic assignment based on department
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"] } } }'
-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"] } } }'
undefinedcurl -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"] } } }'
-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"] } } }'
undefinedJust-in-Time (JIT) Access
即时访问(JIT)
bash
undefinedbash
undefinedAWS: Grant temporary elevated access using STS assume-role
AWS: Grant temporary elevated access using STS assume-role
The user assumes a role that expires after a set duration
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}'
--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}'
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}'
--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)
Okta: Create a time-limited group membership (via API scheduled task)
Add user to admin group
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}"
-H "Authorization: SSWS ${OKTA_API_TOKEN}"
"${OKTA_ORG_URL}/api/v1/groups/${ADMIN_GROUP_ID}/users/${USER_ID}"
curl -s -X PUT
-H "Authorization: SSWS ${OKTA_API_TOKEN}"
"${OKTA_ORG_URL}/api/v1/groups/${ADMIN_GROUP_ID}/users/${USER_ID}"
-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)
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 -
'${OKTA_ORG_URL}/api/v1/groups/${ADMIN_GROUP_ID}/users/${USER_ID}'" | crontab -
undefinedecho "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 -
'${OKTA_ORG_URL}/api/v1/groups/${ADMIN_GROUP_ID}/users/${USER_ID}'" | crontab -
undefinedBreak-Glass Accounts
应急账号
bash
undefinedbash
undefinedCreate break-glass accounts that bypass SSO/MFA for emergency access
Create break-glass accounts that bypass SSO/MFA for emergency access
These accounts must be:
These accounts must be:
1. Excluded from conditional access / MFA policies
1. Excluded from conditional access / MFA policies
2. Protected with extremely long passwords stored in a physical safe
2. Protected with extremely long passwords stored in a physical safe
3. Monitored with alerts on any usage
3. Monitored with alerts on any usage
Azure AD: Create break-glass account
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
--display-name "Break Glass 1"
--user-principal-name "breakglass1@company.onmicrosoft.com"
--password "$(openssl rand -base64 48)"
--force-change-password-next-sign-in false
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
--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
Assign Global Administrator role
az ad group member add --group "SG-BreakGlass" --member-id "$BREAKGLASS_OID"
az ad group member add --group "SG-BreakGlass" --member-id "$BREAKGLASS_OID"
Set up alert on break-glass sign-in (Azure Monitor)
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"
--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"
---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"
--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
9. 审计与合规
Login Audit Logs
登录审计日志
bash
undefinedbash
undefinedGoogle Workspace: Pull login audit logs
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"
fields "actorEmail,ipAddress,loginType,isSecondFactor,isSuspicious"
gam report login user all start "2026-03-01" end "2026-03-24"
fields "actorEmail,ipAddress,loginType,isSecondFactor,isSuspicious"
fields "actorEmail,ipAddress,loginType,isSecondFactor,isSuspicious"
Okta: Query system log for authentication events
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}'
"${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}'
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}'
"${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
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}'
"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}'
undefinedcurl -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}'
"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}'
undefinedAccess Reviews
访问审核
bash
undefinedbash
undefinedList all users and their group memberships for quarterly access review
List all users and their group memberships for quarterly access review
Google Workspace
Google Workspace
gam print group-members fields email,role > /tmp/access-review-groups.csv
gam print group-members fields email,role > /tmp/access-review-groups.csv
Okta: Export all users with their app assignments
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
"${OKTA_ORG_URL}/api/v1/users?limit=200"
| jq -r '.[] | [.profile.email, .status, .lastLogin] | @csv' > /tmp/okta-users.csv
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
"${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
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
"${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
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
"${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
Azure AD: List role assignments
az role assignment list --all --query '[].{principal:principalName, role:roleDefinitionName, scope:scope}' -o table
undefinedaz role assignment list --all --query '[].{principal:principalName, role:roleDefinitionName, scope:scope}' -o table
undefinedCompliance Reporting
合规报告
bash
undefinedbash
undefinedCount of users with/without MFA enrolled
Count of users with/without MFA enrolled
Google Workspace
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
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
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'
"${OKTA_ORG_URL}/api/v1/users?filter=status+eq+"ACTIVE"&limit=200"
| jq '[.[] | select(.credentials.provider.type != "SOCIAL") | .id] | length'
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'
"${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)
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}'
"${OKTA_ORG_URL}/api/v1/users?filter=lastLogin+lt+"${NINETY_DAYS_AGO}"&limit=200"
| jq '.[] | {email: .profile.email, lastLogin: .lastLogin}'
---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}'
"${OKTA_ORG_URL}/api/v1/users?filter=lastLogin+lt+"${NINETY_DAYS_AGO}"&limit=200"
| jq '.[] | {email: .profile.email, lastLogin: .lastLogin}'
---10. Offboarding
10. 员工离职流程
Account Deactivation Checklist
账号停用 checklist
Run this sequence when an employee departs. Order matters -- revoke sessions first, then deactivate.
bash
DEPARTING_USER="alice@company.com"员工离职时执行以下步骤,顺序至关重要——先撤销会话,再停用账号。
bash
DEPARTING_USER="alice@company.com"Step 1: Revoke all active sessions immediately
Step 1: Revoke all active sessions immediately
Okta
Okta
USER_ID=$(curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}"
"${OKTA_ORG_URL}/api/v1/users/${DEPARTING_USER}" | jq -r '.id')
"${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"
-H "Authorization: SSWS ${OKTA_API_TOKEN}"
"${OKTA_ORG_URL}/api/v1/users/${USER_ID}/sessions"
USER_ID=$(curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}"
"${OKTA_ORG_URL}/api/v1/users/${DEPARTING_USER}" | jq -r '.id')
"${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"
-H "Authorization: SSWS ${OKTA_API_TOKEN}"
"${OKTA_ORG_URL}/api/v1/users/${USER_ID}/sessions"
Google Workspace: Revoke tokens and sign out
Google Workspace: Revoke tokens and sign out
gam user "${DEPARTING_USER}" signout
gam user "${DEPARTING_USER}" deprovision
gam user "${DEPARTING_USER}" signout
gam user "${DEPARTING_USER}" deprovision
Azure AD: Revoke all refresh tokens
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"
--url "https://graph.microsoft.com/v1.0/users/${DEPARTING_USER}/revokeSignInSessions"
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"
--url "https://graph.microsoft.com/v1.0/users/${DEPARTING_USER}/revokeSignInSessions"
Step 2: Deactivate the user account
Step 2: Deactivate the user account
Okta
Okta
curl -s -X POST
-H "Authorization: SSWS ${OKTA_API_TOKEN}"
"${OKTA_ORG_URL}/api/v1/users/${USER_ID}/lifecycle/deactivate"
-H "Authorization: SSWS ${OKTA_API_TOKEN}"
"${OKTA_ORG_URL}/api/v1/users/${USER_ID}/lifecycle/deactivate"
curl -s -X POST
-H "Authorization: SSWS ${OKTA_API_TOKEN}"
"${OKTA_ORG_URL}/api/v1/users/${USER_ID}/lifecycle/deactivate"
-H "Authorization: SSWS ${OKTA_API_TOKEN}"
"${OKTA_ORG_URL}/api/v1/users/${USER_ID}/lifecycle/deactivate"
Google Workspace
Google Workspace
gam update user "${DEPARTING_USER}" suspended on
gam update user "${DEPARTING_USER}" suspended on
Step 3: Transfer data ownership
Step 3: Transfer data ownership
Google Workspace: Transfer Drive files
Google Workspace: Transfer Drive files
gam user "${DEPARTING_USER}" transfer drive manager@company.com
gam user "${DEPARTING_USER}" transfer drive manager@company.com
Google Workspace: Transfer Calendar ownership
Google Workspace: Transfer Calendar ownership
gam user "${DEPARTING_USER}" transfer calendar manager@company.com
gam user "${DEPARTING_USER}" transfer calendar manager@company.com
Step 4: Remove from all groups (prevents future provisioning)
Step 4: Remove from all groups (prevents future provisioning)
Okta
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
"${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
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
"${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
Step 5: Revoke app-specific tokens
GitHub: Remove from org
GitHub: Remove from org
gh api -X DELETE "orgs/company/members/${DEPARTING_USER}"
gh api -X DELETE "orgs/company/members/${DEPARTING_USER}"
Slack: Deactivate via SCIM
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')
"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}}]}'
-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}}]}'
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')
"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}}]}'
-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: 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"
--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"
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"
--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
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
undefinedecho "$(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
undefinedPost-Offboarding Verification
离职后验证
bash
DEPARTING_USER="alice@company.com"bash
DEPARTING_USER="alice@company.com"Verify account is suspended/deactivated
Verify account is suspended/deactivated
echo "=== Offboarding Verification ==="
echo "=== Offboarding Verification ==="
Google Workspace
Google Workspace
gam info user "${DEPARTING_USER}" fields suspended | grep -i "suspended: true" && echo "[OK] Google suspended" || echo "[FAIL] Google still active"
gam info user "${DEPARTING_USER}" fields suspended | grep -i "suspended: true" && echo "[OK] Google suspended" || echo "[FAIL] Google still active"
Okta
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"
"${OKTA_ORG_URL}/api/v1/users/${DEPARTING_USER}"
| jq -r '.status' | grep -q "DEPROVISIONED" && echo "[OK] Okta deprovisioned" || echo "[FAIL] Okta still active"
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"
"${OKTA_ORG_URL}/api/v1/users/${DEPARTING_USER}"
| jq -r '.status' | grep -q "DEPROVISIONED" && echo "[OK] Okta deprovisioned" || echo "[FAIL] Okta still active"
GitHub
GitHub
gh api "orgs/company/members/${DEPARTING_USER}" 2>&1 | grep -q "404" && echo "[OK] GitHub removed" || echo "[FAIL] GitHub still member"
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
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}'
"${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}'
---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}'
"${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 | | | |
| Suspend user | | | |
| Enforce MFA | | MFA enrollment policy | Conditional access policy |
| Revoke sessions | | | |
| Audit logins | | | |
| SCIM provision | Built-in for supported apps | App integration SCIM tab | Enterprise app provisioning |
| 任务 | Google Workspace | Okta | Azure AD |
|---|---|---|---|
| 创建用户 | | | |
| 停用用户 | | | |
| 强制实施MFA | | MFA注册策略 | 条件访问策略 |
| 撤销会话 | | | |
| 审计登录记录 | | | |
| SCIM配置 | 支持应用内置配置 | 应用集成SCIM标签页 | 企业应用配置 |