Microsoft Entra ID 租户、应用授权与访问生命周期手册
一个新服务在测试环境完成 OIDC 登录后,上线却持续收到 invalid_issuer。团队检查了 JWT 签名,确认公钥和算法都正确,于是把问题归因于缓存。真正的原因是客户端用了公共 authority,资源端却只允许生产 tenant;令牌是真的,但它来自错误的安全边界。若只问“能不能验签”,多租户应用很容易把不该信任的主体带进系统。
另一个常见现场更隐蔽:员工从企业应用中被移除,SCIM 也把下游账号标成停用,但浏览器会话仍能操作,个人访问令牌仍能调用 API。登录、目录开通、应用内授权和会话撤销是四条链路,任意一条显示成功都不能替其余三条背书。架构设计必须能回答身份来自哪个 tenant、权限落在哪个对象、凭证何时失效,以及哪条日志能证明这些变化。
先把 tenant 当成身份数据的硬边界
Microsoft Entra tenant 是用户、组、应用实例、授权关系和审计记录的顶层目录边界。用户名、邮件地址和显示名都可能重复或改变;业务系统保存外部主体时,应至少把 issuer、tenant 标识和主体对象标识作为复合键,而不是把 email 或裸 sub 当成全局主键。全球云、政府云及其他隔离云的 authority、Microsoft Graph endpoint 和信任根彼此独立,也不能靠替换一个域名片段完成跨云接入。
创建开发环境时,先确定它是 workforce tenant 还是面向外部客户的 external tenant,再通过目标环境实际的 discovery 文档取得 issuer、authorization endpoint、token endpoint 和 JWKS URI。不要把 common、organizations 或一个示例 tenant 直接写进所有环境。公开客户端可以采用授权码加 PKCE;服务端 Web 应用保存回调状态并校验 nonce;后台任务则使用应用身份或联邦工作负载身份。每一种流都要由不同的应用注册、redirect URI 和最小权限承载,避免测试客户端逐渐变成全能生产客户端。
下面的探针只读取发现信息,不需要管理员权限。预期输出中的 issuer 必须与资源端 allowlist 完全一致,jwks_uri 必须位于同一 cloud instance;任一不一致都应在签发或资源端验证阶段失败。
set -euo pipefail
: "${ENTRA_AUTHORITY:?set the approved tenant authority}"
curl -fsS "${ENTRA_AUTHORITY}/v2.0/.well-known/openid-configuration" \
| tee /tmp/entra-discovery.json \
| jq '{issuer,authorization_endpoint,token_endpoint,jwks_uri,scopes_supported}'
issuer=$(jq -r '.issuer' /tmp/entra-discovery.json)
test "${issuer}" = "${EXPECTED_ISSUER}"从 application object 走到 service principal
应用注册主要对应 home tenant 中的 application object。它是应用蓝图,保存 client ID、redirect URI、暴露的 delegated scopes、app roles、所需资源访问和 credential metadata。Graph 返回的 id 是这个目录对象的 object ID,appId 才是 OAuth client ID;两者外观相似,却不能互换。
企业应用主要对应 service principal。它是 application object 在某个 tenant 中的本地实例,用户或组分配、tenant-local 策略、实际 consent、所有者以及许多 SSO 和 provisioning 配置都围绕它发生。多租户应用在客户 tenant 被接受后,会在客户 tenant 形成新的 service principal;home tenant 的对象修改不会神奇地替客户 tenant 重建全部授权关系。Microsoft 的应用对象模型和应用加入目录的过程值得与实际对象清单并排阅读。
清点时要同时查询 application 与 service principal,并把 appId 作为关联字段。只看到应用注册不代表目标 tenant 已安装,只看到 service principal 也不代表它仍符合 home application 的当前定义。
set -euo pipefail
: "${GRAPH_TOKEN:?use a read-only Graph token}"
: "${CLIENT_ID:?set the application client id}"
auth="Authorization: Bearer ${GRAPH_TOKEN}"
curl -fsS -H "$auth" \
"https://graph.microsoft.com/v1.0/applications?%24filter=appId%20eq%20'${CLIENT_ID}'&%24select=id,appId,displayName,signInAudience,requiredResourceAccess" \
| jq '.value[] | {applicationObjectId:.id,appId,displayName,signInAudience}'
curl -fsS -H "$auth" \
"https://graph.microsoft.com/v1.0/servicePrincipals?%24filter=appId%20eq%20'${CLIENT_ID}'&%24select=id,appId,displayName,accountEnabled,servicePrincipalType" \
| jq '.value[] | {servicePrincipalObjectId:.id,appId,displayName,accountEnabled,servicePrincipalType}'授权不是在清单里勾过权限
资源 API 通过自己的 application/service principal 暴露 delegated scope 和 app role。客户端的 requiredResourceAccess 只是“计划请求什么”;它既不是用户或管理员已经同意,也不是资源端已经允许。委托权限最终表现为客户端 service principal 指向资源 service principal 的 oauth2PermissionGrant,并受用户上下文约束;应用权限则表现为资源 app role 到客户端 service principal 的 appRoleAssignment,后台任务不需要用户在线。
这一区分直接决定审计与撤销方式。修改 manifest 后如果真实 grant 没变化,旧权限仍可能继续存在,新权限也不会自动生效。反过来,删除 application object 上的声明也不等于各 tenant 中的 grant 已同步清除。团队应把“请求清单、有效 grant、有效 app role assignment、资源端实际 403/200”做成四列对账,而不是保存一张管理员同意页面截图。权限与同意模型明确区分了委托访问和仅应用访问。
用户 consent 适合低风险、可解释且受策略允许的委托权限;高影响权限应进入管理员审批工作流。即使管理员同意成功,应用仍需在自己的授权层校验角色、组织和资源归属。Entra 的 directory role、Microsoft Graph application permission、Azure RBAC role 与业务 app role 是不同控制平面,名称相近不能互相替代。
组、应用角色与 SCIM 要分开建模
组可以组织目录成员、给企业应用做 assignment、承载 app role、进入 token claim,也可能被 SCIM 推送到下游。一个组同时承担这些职责会把组织调整变成授权变更:HR 重命名或合并部门组,应用权限就可能无意扩散。更稳妥的做法是建立应用专用组或直接使用 app role,以对象 ID 绑定策略;显示名只用于界面展示。
资源端不能假设 token 总含完整 groups。成员关系较多、流类型不同或 claim 配置变化时,令牌可能给出 overage 指示,应用需要按已验证的主体调用 Graph 查询,或采取拒绝策略。不要截断列表后继续授权,也不要在每次请求中无缓存地遍历目录。缓存键必须包含 tenant 与主体,失效策略要跟组变更、token 生命周期和风险预算一致。具体行为应通过组声明配置文档和目标 tenant 的真实 token 校验。
SCIM 负责账号与组的生命周期同步,不负责 OIDC 登录,也不会自动撤销业务会话。建立 non-gallery provisioning 时,先读取目标的 /ServiceProviderConfig、/Schemas 与 /ResourceTypes,再确认 matching attribute、externalId、PATCH、Group、停用和重新启用语义。id 由目标服务分配,externalId 由客户端提供;二者倒置会导致重复账号或误覆盖。生产初启只分配 canary 用户与应用专用组,观察完整 create、update、group add/remove、deactivate、rehire 链路后再扩大 assignment。
set -euo pipefail
: "${SCIM_BASE_URL:?set the test SCIM endpoint}"
: "${SCIM_TOKEN:?load from a secret manager}"
auth="Authorization: Bearer ${SCIM_TOKEN}"
curl -fsS -H "$auth" "${SCIM_BASE_URL}/ServiceProviderConfig" | jq .
curl -fsS -H "$auth" "${SCIM_BASE_URL}/Schemas" | jq '.Resources[].id'
curl -fsS -G -H "$auth" \
--data-urlencode 'filter=userName eq "entra-canary@example.invalid"' \
"${SCIM_BASE_URL}/Users" \
| jq '{schemas,totalResults,Resources}'不存在的 canary 应返回合法 ListResponse 和空结果。错误 token 应得到 401 或 403,非法 filter 应得到规范化错误而不是全量用户。连接测试通过只说明端点可达;只有逐状态验证并保存请求 ID、目标对象 ID 和 provisioning log,才能证明生命周期契约成立。
工作负载身份要同时收紧信任与资源权限
普通 application/service principal 可以用 client secret 或证书换取 token;managed identity 则由 Azure 资源提供方管理生命周期,并在目录中表现为特殊 service principal。federated identity credential 把外部 issuer、subject、audience 映射到 token exchange 信任,使 CI、Kubernetes 或其他工作负载不必保存长期 Entra secret。Workload ID 文档给出了这些对象的入口,但它们并不自动授予资源权限。
联邦配置至少有两道门。第一道验证外部 token 的 issuer、subject、audience 和签名;第二道由目标资源的 app role、Azure RBAC 或 ACL 决定能做什么。只测“成功拿到 access token”会漏掉过宽角色,只测资源调用则可能把错误 issuer 被其他 credential 接受的风险藏起来。负向实验应逐一改错 issuer、audience、subject 和 key,再撤掉资源角色;预期分别在 token exchange 与资源 API 阶段失败,并能在 service principal sign-in 或资源日志中找到对应证据。
managed identity 的所有者是 Azure 资源,不应把它当普通 app registration 随意删除、补 credential 或跨环境复制。迁移资源时重新建立身份与角色绑定,再让旧、新身份短暂双跑;确认新身份的调用量和拒绝原因稳定后,先撤旧角色,再删除旧资源,避免留下无法归属的 service principal。
项目接入要把身份验证和业务授权分层
后端服务启动时读取 discovery,并按 issuer 获取 JWKS;请求到达后依次验证签名、算法、issuer、audience、有效期以及所选流要求的 azp、nonce 或其他约束。验证通过只得到“某个 tenant 签发了一个给当前资源的 token”,业务授权还要把 roles、受控 group 或内部 entitlement 映射到具体操作。前端不应把 ID token 当 API access token,API 也不应接受发给 Microsoft Graph 或另一个服务的 token。
下面是 Spring Security 的最小资源端形态。生产配置中的 issuer 从部署环境注入,allowlist 由平台配置管理;多 tenant 场景需要按 issuer 选择独立的 AuthenticationManager,不能只解析 tid 后跳过 issuer 验证。
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ${ENTRA_ISSUER_URI}
identity:
expected-audience: ${ENTRA_API_AUDIENCE}
allowed-tenants: ${ENTRA_ALLOWED_TENANTS}
role-prefix: "APP_"应用内部建议保存 issuer + oid/tid 到本地主体映射,并记录授权版本。映射不存在时进入显式邀请或审批,而不是按邮箱自动合并。group 或 app role 变化后,新的 token 应改变授权结果;高风险操作还可查询实时 entitlement 或要求重新认证。日志只记录 token 指纹、client ID、tenant、主体对象 ID、决策规则和 correlation ID,不记录完整 token 与敏感 claims。
用最小正反实验证明配置真的生效
正向实验创建一个无业务数据权限的 canary 用户、一个应用专用组和一个低风险 app role。用户登录后,资源 API 应看到期望 issuer、audience 与角色,访问允许的只读端点成功;同一请求的 sign-in 记录、API correlation ID 和应用授权日志能够关联。随后通过企业应用 assignment 去掉 canary,并取得新 token,受保护端点应拒绝。旧 token 是否立即失效取决于会话与资源策略,因此必须单独执行会话撤销或缩短高风险会话,并观察真实结果。
反向实验使用同一个 client 向错误 tenant 请求 token,或者把资源端 audience 改成另一个 API。签名仍可能有效,但资源端必须因 issuer 或 audience 不匹配而拒绝。第二组反例保留 consent 却移除 app role assignment,预期 token 获取与资源访问在不同阶段呈现失败;第三组反例制造重复 SCIM matching attribute,provisioning job 应明确失败或隔离,不能任意覆盖已有用户。
set -euo pipefail
: "${ACCESS_TOKEN:?obtain a short-lived canary token}"
: "${API_BASE_URL:?set the isolated test API}"
curl -fsS -D /tmp/entra-ok.headers \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "X-Correlation-ID: ${CORRELATION_ID}" \
"${API_BASE_URL}/identity/whoami" | jq '{issuer,tenant,subject,roles}'
status=$(curl -sS -o /tmp/entra-denied.json -w '%{http_code}' \
-H "Authorization: Bearer ${WRONG_AUDIENCE_TOKEN}" \
"${API_BASE_URL}/identity/whoami")
test "$status" = "401" -o "$status" = "403"
jq '{error,correlationId}' /tmp/entra-denied.json实验结束按依赖反序清理:撤销会话与 refresh token、移除 grant/assignment、停用 provisioning job、撤销 SCIM credential、删除 canary 对象,最后确认旧 token 或 credential 不能再完成原调用。只删除用户会留下应用授权,只删除应用注册又可能留下目标 tenant 的企业应用实例;对象清单必须在清理后再次读取。
日志证据要覆盖变更、签发、调用与同步
Entra 的 audit log 记录目录对象和策略变更;sign-in 又分用户交互、非交互、service principal 与 managed identity 等类别;provisioning log 解释 SCIM 匹配、映射与下游结果;Microsoft Graph activity 和资源日志补上 API 调用证据。值班排障先按 correlation ID、request ID、application/service principal object ID 和主体对象 ID 串联,不要只按显示名搜索。
set -euo pipefail
: "${GRAPH_TOKEN:?use an approved audit reader token}"
auth="Authorization: Bearer ${GRAPH_TOKEN}"
curl -fsS -H "$auth" \
'https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?%24top=10' \
| jq '.value[] | {id,activityDateTime,activityDisplayName,result,correlationId,targetResources}'
curl -fsS -H "$auth" \
'https://graph.microsoft.com/v1.0/auditLogs/signIns?%24top=10' \
| jq '.value[] | {id,createdDateTime,appId,resourceId,servicePrincipalId,status,correlationId}'内置查询窗口会随日志类型、许可和平台策略变化,不能充当合规归档。事件仍可读取时就应外送到受控日志平台,保存原始事件 ID、原始时间、actor、target、issuer/tenant、应用对象、结果与 correlation ID;消费端用事件 ID 去重,以游标或平台提供的 continuation 续读,并监控迟到、重复、解析失败和最后成功 checkpoint。PII、设备、位置、风险与 modified properties 要分级授权,搜索索引和冷存储都不能默认向全体开发者开放。
权限、凭证、数据边界与团队所有权
应用 owner 不是装饰字段。每个 application、service principal、credential、consent、组、SCIM connector 和日志导出都应有业务 owner 与平台 owner,离职时通过群组或受控角色交接,不能依赖单个创建者账号。日常自动化使用最小 Graph 权限和独立 workload identity;紧急管理身份启用强认证、审批和短时授权,操作进入审计。
client secret、证书私钥、federated token、refresh/access token、Graph token 和 SCIM bearer token 都不得进入仓库、命令历史、CI 输出或截图。证书和联邦身份优先于长期 secret,但仍需轮换 key、限制 subject、验证 audience,并为失效演练保留第二条受控路径。credential metadata 可盘点到期与 owner,私钥只能留在密钥系统或不可导出的受管载体中。
目录日志和 SCIM payload 含用户属性、组关系、IP、设备与风险信息。数据最小化不仅是少发几个 claim,还包括缩小同步属性、控制日志字段、隔离生产与测试 tenant、限制支持人员查询,以及定义删除和保留策略。跨区域、跨 cloud 或交给外部 SaaS 前,应由合同、数据驻留和合规要求决定架构,不能从开发 tenant 的可用功能推导生产结论。
容量、成本与可用性从动态能力读取
Entra 是 SaaS 控制平面,团队不负责部署它的数据库副本,却仍要为依赖故障设计退化。应用可缓存已验证 JWKS 并在 key 轮换时刷新;短暂 discovery 或 Graph 故障不应让已有低风险会话瞬间全部失效,但不能无限延长授权缓存。SCIM、Graph 查询和日志导出都要遵守响应头与服务返回的节流信号,采用有抖动的退避、幂等写入和 checkpoint,避免失败重试放大成目录风暴。
许可、日志窗口、API 限额、组声明行为、云区域能力和控制台入口都会变化。容量评审应从目标 tenant 的服务限制、实际响应头、Graph 元数据、合同与当前产品文档读取,不在代码里固化套餐名称或通用数字。成本模型至少纳入活跃身份、所需安全能力、日志外送与存储、SCIM 连接器、跨区域合规、支持等级和迁移双跑;免费开发入口只能验证协议和对象模型,不能代表生产权益。
可用性演练要观察不变量:issuer allowlist 不因发现服务异常而放宽,JWKS 轮换后新旧 key 在预期过渡内可验证,Graph 限流时授权缓存不越权,SCIM 重试不重复建人,日志 checkpoint 恢复后事件计数和哈希抽样一致。告警阈值应由请求基线、SLO 与目录变更频率决定,而不是复制一个脱离租户规模的数字。
撤销、清理与迁移必须按对象依赖反序执行
人员离职先阻止新的交互登录与 token 获取,再撤应用 assignment、敏感组和 directory role,触发 SCIM 停用,撤销 refresh token 与业务会话,并回收应用内 entitlement、API token 和许可证。每一步都保留对象 ID 与事件证据。active=false 只证明下游目录状态改变,不证明浏览器 cookie、服务令牌或业务数据访问已经结束。
应用退出先冻结新 consent 和新 assignment,盘点所有 tenant-local service principal、delegated grant、app role assignment、credential、federated identity credential、redirect URI、SCIM connector 与日志订阅。流量切走并确认新身份链稳定后,先撤资源角色与 grant,再撤 credential、停止 provisioning、删除本地实例,最后处理 home application。删除顺序相反可能让残留授权失去可读的归属对象。
tenant-to-tenant 或跨产品迁移会改变 issuer、对象 ID、client credential、redirect URI、签名 key 和信任关系。用户密码、MFA、设备、会话、consent 与应用内 entitlement通常不能视为可透明搬运。实践中应建立旧、新主体映射和双发行验证,按应用专用组迁移小批 canary,比较登录、授权、SCIM 与日志证据;冻结旧侧写入后完成最终增量,再撤旧信任。回退保留的是可审计的旧路径和明确截止条件,而不是长期并存的两个全权身份源。
