| name | performing-oauth-scope-minimization-review |
| description | 执行 OAuth 2.0 权限范围最小化审查,识别过度授权的第三方应用集成、 过多的 API 范围、未使用的令牌授权以及跨身份提供商和 SaaS 平台的 高风险 OAuth 同意模式。 适用于 OAuth 范围审计、API 权限审查、第三方应用风险评估或同意授权最小化的请求。
|
| domain | cybersecurity |
| subdomain | identity-access-management |
| tags | ["OAuth","scope-minimization","API-security","consent-review","third-party-risk","token-audit"] |
| version | 1.0 |
| author | mahipal |
| license | Apache-2.0 |
执行 OAuth 权限范围最小化审查
适用场景
- 对第三方应用 OAuth 权限的年度或季度审查
- 涉及 OAuth 令牌泄露或未授权数据访问的安全事件发生后
- 需要记录第三方数据访问情况的合规审计(GDPR 第 28 条、SOC 2)
- 发现影子 IT 应用通过 OAuth 授权访问组织数据
- 需要权限清理的 SaaS 应用迁移或整合
- 为 API 集成实施最小权限原则
不适用于审查同一信任边界内第一方应用的权限;OAuth 范围最小化专注于第三方和跨边界的同意授权。
前提条件
- 对身份提供商(Microsoft Entra ID、Okta、Google Workspace)的管理员访问权限
- Microsoft Graph API 权限:Application.Read.All、OAuth2PermissionGrant.ReadWrite.All
- 来自采购或 IT 治理的已批准第三方集成清单
- OAuth 范围风险分类框架
- 令牌分析工具(手动审查使用 jwt.io,批量分析使用自动化脚本)
工作流程
步骤 1:清点所有 OAuth 授权和同意权限
枚举所有 OAuth 应用注册及委托权限:
"""
OAuth 授权清点 - Microsoft Entra ID
枚举所有应用注册、服务主体、
委托权限及应用权限授予情况。
"""
import requests
import json
from collections import defaultdict
class EntraOAuthAuditor:
def __init__(self, tenant_id, client_id, client_secret):
self.tenant_id = tenant_id
self.base_url = "https://graph.microsoft.com/v1.0"
self.token = self._get_token(client_id, client_secret)
self.headers = {"Authorization": f"Bearer {self.token}"}
def _get_token(self, client_id, client_secret):
url = f"https://login.microsoftonline.com/{self.tenant_id}/oauth2/v2.0/token"
response = requests.post(url, data={
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
"scope": "https://graph.microsoft.com/.default"
})
return response.json()["access_token"]
def get_all_service_principals(self):
"""获取所有服务主体(企业应用)。"""
apps = []
url = f"{self.base_url}/servicePrincipals?$top=999&$select=id,appId,displayName,appOwnerOrganizationId,servicePrincipalType,accountEnabled,createdDateTime"
while url:
response = requests.get(url, headers=self.headers)
data = response.json()
apps.extend(data.get(, []))
url = data.get()
apps
():
grants = []
url =
url:
response = requests.get(url, headers=.headers)
data = response.json()
grants.extend(data.get(, []))
url = data.get()
grants
():
url =
response = requests.get(url, headers=.headers)
response.json().get(, [])
():
service_principals = .get_all_service_principals()
delegated_grants = .get_oauth2_permission_grants()
sp_map = {sp[]: sp sp service_principals}
inventory = []
grant delegated_grants:
sp = sp_map.get(grant[], {})
scopes = grant.get(, ).split()
scope scopes:
scope:
inventory.append({
: sp.get(, ),
: grant.get(),
: sp.get(),
: sp.get() != .tenant_id,
: ,
: scope,
: grant.get(),
: grant.get(),
: sp.get(),
: sp.get(, )
})
sp service_principals:
app_roles = .get_app_role_assignments(sp[])
role app_roles:
inventory.append({
: sp.get(),
: sp.get(),
: sp.get(),
: sp.get() != .tenant_id,
: ,
: role.get(),
: ,
: role.get(),
: sp.get(, )
})
inventory
步骤 2:按风险等级对 OAuth 范围分类
根据数据访问敏感性对权限进行分类:
"""
OAuth 范围风险分类
根据数据敏感性和访问广度将 API 范围映射到风险等级。
"""
MICROSOFT_GRAPH_SCOPE_RISK = {
"critical": {
"scopes": [
"Directory.ReadWrite.All",
"RoleManagement.ReadWrite.Directory",
"Application.ReadWrite.All",
"AppRoleAssignment.ReadWrite.All",
"Mail.ReadWrite",
"Mail.Send",
"Files.ReadWrite.All",
"Sites.FullControl.All",
"User.ReadWrite.All",
"Group.ReadWrite.All",
"MailboxSettings.ReadWrite",
"full_access_as_app",
],
"risk_description": "可读写所有数据、修改目录或模拟用户",
"review_frequency": "每月",
"requires_admin_consent": True
},
"high": {
"scopes": [
"Mail.Read",
"Mail.Read.Shared",
"Calendars.ReadWrite",
"Contacts.ReadWrite",
"Files.Read.All",
"Sites.Read.All",
"User.Read.All",
"Group.Read.All",
"Directory.Read.All",
"AuditLog.Read.All",
"SecurityEvents.ReadWrite.All",
"TeamSettings.ReadWrite.All",
],
"risk_description": "对组织敏感数据具有广泛读取访问",
"review_frequency": ,
:
},
: {
: [
,
,
,
,
,
,
,
,
,
],
: ,
:
},
: {
: [
,
,
,
,
,
,
,
,
],
: ,
:
}
}
():
risk_level, config MICROSOFT_GRAPH_SCOPE_RISK.items():
scope config[]:
{
: scope,
: risk_level,
: config[],
: config[]
}
{
: scope,
: ,
: ,
:
}
():
risk_weights = {: , : , : , : }
total_score =
classified_scopes = []
perm app_permissions:
classification = classify_scope_risk(perm[])
classified_scopes.append(classification)
total_score += risk_weights.get(classification[], )
app_type_permissions = [p p app_permissions p[] == ]
total_score += (app_type_permissions) *
admin_consent = [p p app_permissions p[] == ]
total_score += (admin_consent) *
total_score >= :
aggregate_risk =
total_score >= :
aggregate_risk =
total_score >= :
aggregate_risk =
:
aggregate_risk =
{
: total_score,
: aggregate_risk,
: (app_permissions),
: ([s s classified_scopes s[] == ]),
: ([s s classified_scopes s[] == ]),
: classified_scopes
}
步骤 3:识别过度授权的应用
检测请求权限超出功能需求的应用:
"""
过度授权检测
识别 OAuth 范围相对于其功能而言过度的应用。
"""
def detect_over_permissions(inventory, approved_apps_catalog):
"""
将实际权限与已批准的范围目录进行比对,
找出过度授权的应用。
"""
findings = []
app_permissions = defaultdict(list)
for perm in inventory:
app_permissions[perm["app_name"]].append(perm)
for app_name, permissions in app_permissions.items():
approved = approved_apps_catalog.get(app_name)
if not approved:
findings.append({
"app_name": app_name,
"finding_type": "UNAPPROVED_APPLICATION",
"severity": "HIGH",
"detail": f"应用不在已批准目录中,共有 {len(permissions)} 个权限授予",
"scopes": [p["scope"] for p in permissions],
"recommendation": "审查并批准或撤销所有权限"
})
continue
approved_scopes = set(approved.get("approved_scopes", []))
actual_scopes = set(p["scope"] for p in permissions)
excessive = actual_scopes - approved_scopes
if excessive:
risk = analyze_app_risk([p for p in permissions p[] excessive])
findings.append({
: app_name,
: ,
: risk[],
: ,
: (excessive),
: (approved_scopes),
:
})
unused = approved_scopes - actual_scopes
unused:
findings.append({
: app_name,
: ,
: ,
: ,
: (unused)
})
broad_patterns = [
(, , ),
(, , ),
(, , ),
(, , ),
]
broad, narrow, description broad_patterns:
broad actual_scopes:
findings.append({
: app_name,
: ,
: ,
: description,
: broad,
: narrow,
:
})
findings
步骤 4:审计令牌使用情况并检测过期授权
识别不再活跃使用的 OAuth 令牌:
"""
令牌使用情况审计
分析登录日志和 API 活动,识别过期的 OAuth 授权。
"""
def audit_token_usage(auditor, days_inactive=90):
"""识别近期无 API 活动的 OAuth 授权。"""
url = f"{auditor.base_url}/auditLogs/signIns"
params = {
"$filter": f"createdDateTime ge {(datetime.utcnow() - timedelta(days=days_inactive)).isoformat()}Z and signInEventTypes/any(t: t eq 'servicePrincipal')",
"$top": 999
}
active_apps = set()
while url:
response = requests.get(url, headers=auditor.headers, params=params)
data = response.json()
for signin in data.get("value", []):
active_apps.add(signin.get("appId"))
url = data.get("@odata.nextLink")
params = {}
all_grants = auditor.get_oauth2_permission_grants()
sp_map = {sp["id"]: sp for sp in auditor.get_all_service_principals()}
stale_grants = []
for grant in all_grants:
sp = sp_map.get(grant["clientId"], {})
app_id = sp.get("appId")
if app_id and app_id not in active_apps:
stale_grants.append({
"app_name": sp.get("displayName", "Unknown"),
"app_id": app_id,
"scopes": grant.get("scope", "").split(),
"consent_type": grant.get(),
: sp.get() != auditor.tenant_id,
: days_inactive,
:
})
(stale_grants, key= x: (x[]), reverse=)
步骤 5:生成修复计划并执行范围缩减
创建并执行范围最小化修复计划:
"""
OAuth 范围修复
生成并执行范围缩减操作。
"""
def generate_remediation_plan(findings, stale_grants):
"""创建优先级排序的修复计划。"""
plan = []
for f in findings:
if f["finding_type"] == "UNAPPROVED_APPLICATION":
plan.append({
"priority": 1,
"action": "REVOKE_ALL_PERMISSIONS",
"app_name": f["app_name"],
"reason": "未批准的第三方应用",
"impact": f"移除 {len(f['scopes'])} 个权限授予",
"risk_if_not_addressed": "CRITICAL"
})
for f in findings:
if f["finding_type"] == "EXCESSIVE_SCOPES":
plan.append({
"priority": 2,
"action": "REMOVE_EXCESSIVE_SCOPES",
"app_name": f["app_name"],
"scopes_to_remove": f["excessive_scopes"],
"reason": "超出已批准目录的范围",
"risk_if_not_addressed": f["severity"]
})
for f findings:
f[] == :
plan.append({
: ,
: ,
: f[],
: f[],
: f[],
: f[],
:
})
grant stale_grants:
plan.append({
: ,
: ,
: grant[],
: grant[],
: ,
:
})
(plan, key= x: x[])
():
url =
response = requests.get(url, headers=auditor.headers)
current_grant = response.json()
current_scopes = (current_grant.get(, ).split())
updated_scopes = current_scopes - (scopes_to_remove)
updated_scopes:
requests.delete(url, headers=auditor.headers)
{: , : grant_id}
:
update_body = {: .join(updated_scopes)}
requests.patch(url, headers=auditor.headers, json=update_body)
{
: ,
: grant_id,
: (scopes_to_remove),
: (updated_scopes)
}
核心概念
| 术语 | 定义 |
|---|
| OAuth 范围 | 定义授予客户端应用的特定 API 访问级别的权限字符串(例如:Mail.Read、Files.ReadWrite.All) |
| 委托权限 | 代表已登录用户行使的 OAuth 范围,受应用权限和用户自身访问权的双重限制 |
| 应用权限 | 无需用户上下文直接授予应用的 OAuth 范围,可访问所有用户数据(高风险) |
| 管理员同意 | 由管理员做出的租户级权限授予,无需个人同意即可应用于所有用户 |
| 范围最小化 | 将 OAuth 权限减少到应用功能所需最小集合的安全原则 |
| 过期授权 | 保持活跃但近期无 API 使用记录的 OAuth 权限,表明集成已废弃或过时 |
工具与系统
- Microsoft Entra 管理中心:用于审查企业应用、同意权限和 OAuth 授权管理的门户
- Nudge Security:用于发现 OAuth 授权、评估第三方风险并自动化范围审查的 SaaS 安全平台
- Cerby:用于审计 OAuth 集成和管理共享账户的非 SSO 应用管理平台
- Microsoft Graph API:用于大规模枚举和修改 OAuth 权限授予的编程接口
常见场景
场景:安全事件后的 OAuth 范围审计
背景:一次钓鱼攻击导致管理员账户被入侵,调查发现攻击者注册了一个带有 Mail.ReadWrite 和 Files.ReadWrite.All 范围的恶意 OAuth 应用,窃取了 6 个月的电子邮件。组织需要进行全面的 OAuth 范围审查。
处理方法:
- 立即撤销被入侵管理员会话的所有 OAuth 授权
- 枚举租户内所有服务主体和权限授予
- 标记过去 90 天内注册的所有应用进行手动审查
- 使用风险框架对所有第三方应用范围进行分类
- 识别具有严重范围的应用(Mail.ReadWrite、Files.ReadWrite.All、Directory.ReadWrite.All)
- 与 IT 采购的已批准应用目录进行交叉对比
- 立即撤销所有未批准应用的权限
- 将过度授权的已批准应用降级为所需最小范围
- 实施管理员同意工作流,防止未来出现不受控的 OAuth 授权
- 启用同意策略,要求对高风险范围进行管理员审批
注意事项:
- 未经协调撤销业务关键集成的权限会导致服务中断
- 未检查应用级权限(vs 委托权限),这类权限风险更高且常被忽视
- 遗漏多租户应用,即发布者租户与使用租户不同的情况
- 修复后未实施持续监控,导致无法检测新的未授权 OAuth 授权
输出格式
OAUTH 范围最小化审查报告
=========================================
租户: corp.onmicrosoft.com
审查周期: 2026-02-01 至 2026-02-24
应用总数: 147
第三方应用: 98
第一方应用: 49
权限清单
OAuth 授权总数: 487
委托权限: 312
应用权限: 175
管理员同意: 89
用户同意: 223
风险分类
严重风险应用: 7
- UnknownCRMApp (Mail.ReadWrite, Files.ReadWrite.All - 未批准)
- LegacySync (Directory.ReadWrite.All - 过多权限)
- DevToolX (Application.ReadWrite.All - 权限过于宽泛)
高风险应用: 18
中风险应用: 34
低风险应用: 88
发现问题
未批准应用: 12(立即撤销)
过多范围: 23 个应用的范围超出已批准列表
过于宽泛的权限: 15 个应用可降级处理
过期授权(90天以上): 31 个应用近期无 API 活动
修复计划
优先级 1(立即): 12 个未批准应用撤销
优先级 2(本周): 23 个过多范围移除
优先级 3(本月): 15 个范围降级
优先级 4(下季度): 31 个过期授权撤销
预计范围缩减: 总权限的 34%