-
为什么更新本篇?
-
数字世界面临的威胁(从传统漏洞攻防进入身份强对抗阶段)
-
第一个阶段以服务端漏洞为主,主要漏洞包括单不局限IIS Unicode编码漏洞、MS01-033 IIS远程溢出漏洞、SolarIs RPC远程溢出漏洞、RPC/DCOM漏洞等等,这个时代的特点就是挖掘漏洞的人对系统本身的安全了解程度非常深入,而且非常扎实,这些人笔者理解主要追求自身对于底层操作系统的着迷,属于纯技术追求主义者; -
第二个阶段是以客户端相关的漏洞为主,主要的漏洞包括(浏览器漏洞、Adobe Flash漏洞、PDF漏洞等等),这个时代的特点是开始出现了偏向获取利益的相关攻击团体,用客户端漏洞作为跳板进而渗透进入特定目标,获取相应信息谋取利润; -
第三个阶段是以WEB 2.0、商业化软件漏洞为主,主要的漏洞包括传统的WEB 2.0应用层漏洞,以及商业化软件漏洞(Citrix、VPN、MFT文件传输、SharePoint、Exchange等)为主,这个阶段的特点是慢慢地以国家级攻击团体为主的形态,在各个领域获取对应的情报; -
第四个阶段也就是现在这个阶段,是以云平台为主要的攻击手段,因为国内外逐步上云,尤其是漂亮国公有云为主,而且有很明确的数据统计,从笔者2019年观察的25%目前已经到了88%以上的公有云接入率,而在其中发展最快的当属Microsoft Azure云,由于Microsoft Azure云的商业属性、Windows的江湖地位、Office办公的普及,让Azure快速成为TOP 500强和世界各国上云的首选目标,随着客户量的增加这两年针对Azure云平台的攻击呈现爆发式趋势,出现了大量的高危漏洞(SolarWinds Golden SAML2、SynLapse、MFA绕过、TPM绕过、ChaosDB、OIMGod、Password Spraying)等等众多国家级黑客利用的超难度漏洞。笔者针对这个阶段总结是从传统攻击转向身份攻击的一个阶段,从各种报告和针对性的研究也可以看出来,微软这些年针对MIP平台的设计一直在做深入的迭代和攻防对抗。其实笔者有个很敏锐的观点,就是安全虽然是纵深防御的,但是总会在某个点上做了强的对抗,笔者称之为“安全防御高强度对抗点”,通过这个点的对抗设计,来保证整个安全的体系设计,其实这个案例也有几个,例如TPM、MFA等,都称得上是“安全防御高强度对抗点”,这个点必然成为国家级攻击团体的必争之地,突破了这个点也就是对整个云平台会造成重大的影响,所以这也是MIP成为众矢之的对抗核心的点,也是笔者为什么一定要写透彻这个点的设计,一方面攻击角度是这是云上的真正战场,另外一个是防御角度从国家层面来看,身份的攻击必然是提升国家水位的根基(漂亮过拜登推进Executive Order on Improving the Nation’s Cybersecurity其中的推进零信任是整个国家安全水位提升的根基)。
-
漏洞情报:(CISA发布了786个APT组织常用的漏洞),这些漏洞都会进入国家级攻击团体的攻击漏洞的列表; -
暗网情报:例如出售的凭证用来访问内部网络(Lapsus$的Okta、微软BING代码泄露、英伟达代码泄露、三星代码泄露)的各种情报进行渗透; -
对云基础设施的了解程度加深:随着越来越多的人了解云平台设计细节之后,开始大规模的利用云的各种漏洞来突破身份; -
国外反APT的情报:国外有众多发布APT报告的厂商FireEye、Mandiant、PaloAlto Networks、IBM X-Force、Cisco Talos、Microsoft Security Inteligence Report等挖掘了大量的Attack Frontline高水平情报,整理输出成威胁情报和报告发布业界,用来震慑和体现APT的防御水位,在此情况下国家级攻击者也在以同样的观察、分析、组织、研究等能力做储备; -
国家共享的情报:尤其是漂亮国从国家、私有企业、安全公司、政府等不断发布威胁情报,这些威胁情报包括Unclassfic的DHS AIS、FS-ISAC、H-ISAC、S-ISAC等几十个ISAC情报组织,到Secret和Top Secret的情报分级TLP、NXA,CXA,DXS内部分享的更高阶攻击情报,都会是国家级攻击者重要研究资源; -
独有的技术趋势研究:国家级攻击团体也在逐步研究业界的趋势,包括基础设施的、底层网络组件的、虚拟化的,他们都有很多部门和时间来做针对性的研究和跟进; -
独有的安全漏洞挖掘能力:国家级攻击团体也会依靠生态、漏洞情报ZDI、地下黑市和暗网论坛购买顶级漏洞,包括操作系统级别、浏览器级别等等的漏洞,包括在暗网购买对应的凭证和泄露的数据为下一步动作做储备; -
独有的联动组织:国家级攻击团队的对抗都是依靠各个部门的联动和协同,例如漂亮国实施的震网行动,就是依靠NXA的研究能力和CXA的人员情报分析、研判、震网病毒的人肉更新等,有充足的资源来调动高阶攻击;
-
MIP整体设计思路 -
业务角度&战略布局: -
从业务角度也就是战略布局角度,Microsoft一直主打”MutiCloud & Hybrid Cloud“混合云战略,其中也包括多云可以算是混合云的一种,其实纵观Microsoft的整个产品布局,基本上都是在走这个逻辑,举几个例子帮助读者理解:云无处不在:不管是边缘云(Edge)、混合云、多云,微软一直在构建无处不在的云,例如应用态通过开源Serverless生态运行在AWS、Google的无服务多云架构、安全态通过Azure Sentinel CloudSIEM做的多云和混合云的安全监控、身份态通过云上Azure Active Directory和On Permises Active Directory构建的混合认证身份、网络态通过VPN Express构建的线下和云上打通的一体化网络形态、研发生态通过Github、Visual Studio、Azure Cloud构建的研发形态、数据态运行在各个云上和线下的SQL Server以及对应的Purview数据安全治理形态; -
从自身管理角度考虑:为了应对如此复杂的商业模式、商业产品、庞大的商业生态、复杂的全球客户交付、众多的内部人员、庞大的交付团队来说,需要设计完整的有利于自己的模式,其实微软也是赶上了很好的时机,正好云的战略赌对、很多的ToB基因、复杂设计的简单化以及良好的设计思想、加上漂亮国的SaaS的20年的发展,都使得微软可以设计基于SaaS的,全部业务都在线的一种设计模式和思路,使得MIP逐步发展起来; -
企业基因角度:微软一直是以ToB深耕Windows、云等业务,通过前20年的Windows积累,储备了大量的ToB基因,而且Windows本身也是一个复杂的生态问题(硬件厂商、软件厂商、固件厂商、应用厂商)等,所以云业务形成现在的一种模式,也是经验的一种沉淀。通过之前积累的复杂生态问题,笔者猜测落地了一套完善的内部管理逻辑,用来管理复杂的Supply Chain、Third Partner等管理,基于这套多年的沉淀,做到了从操作系统到云操作系统的转移,毕竟云的未来还是一台计算机(OneComputer)的逻辑,只不过这个”计算机”的规模和复杂度更高而已。 -
业务模式和趋势变化:其实前面说到,数字化转型、SaaS、API化已经成为了一种业务必然的模式,在这种业务模式下首先解决自身的数字化之后,发现可以通过身份来打通生态,身份代表的是就是一家企业、一个合作伙伴、一个售后的销售业务等,发现都可以把传统的销售、实施等环节线上化,这个是过程中体会到的重要思维变化; -
企业发展过程中思维的变化:其实从下面的图片也可以窥得一些设计思路,数字化转型的状态下老的企业都是以自身的员工为着力点,其实微软提出员工、合作伙伴、客户等是身份要面对的全部人的时候,相信业务上也做出了对应的决策了,就如前面提到的SaaS化、数字化转型等来应对微软自身和云的复杂的体系,再往前走一步思考,那既然微软自己都可以这样落地,自身这么复杂的生态都支持了,何不扩展到更多的客户,让客户也享受这种红利,而且也能体会业界的供应商的厂商和大型客户才会购买这样的复杂的适合自身的业务模式转变。另外几个点就是微软本身的复杂业务场景导致的一些变化,例如之前大家是不是都经历了都使用公司发放笔记本的状态,而现在越来越多的使用Pad、BYOD等设备来处理一些工作;应用形态上也发生了变化,原来都是单体的应用,部署在线下,逐步也可以往公有云迁移,而且一切都在API化,API的作用就是整个业务的粘合剂,通过API来构建企业的ECOSystem;原来的安全相信读者都也经历过都是以公司防火墙和公司网络为主,而现在数据无处不在,传统的边界已经消失;
-
MIP身份发展路径
支撑其他电路板、器件和器件之间的相互连接,并为所支撑的器件提供电源和数据信号的电路板或框架,所以用粘合剂来说特别合适,通过身份粘合微软自身体系的所有业务,是基础底座)。
-
MIP产品设计思路 -
登录:首先有统一的登录界面,login.microsoftonline.com来进行全球的SSO单点登录; -
认证:通过OAuth2、OpenID进行身份认证,并且支持相当复杂的SaaS、On-Permises、商业软件、安全软件等统一认证登录; -
目录服务:通过支持Azure AD和Microsoft Active Directory等作为整个登录的目录中心,用来存储统一的信息; -
MFA:支持短信认证、FIDO2、软硬令牌、Windows Hello等认证方式; -
开源代码库:通过MSAL开源代码身份库,用来支撑庞大的生态和客户接入,支持的版本包括JS、JAVA、.NET等,通过MSAL可以让大B客户自己的应用进行集成; -
前后分离:微软明显给大家普及了前后端分离的逻辑,后端都是使用API来进行前台的展台,API都是通过微软的控制台进行注册,然后通过本身讲述的Graph API以及支持其他API的方式进行验证。 -
复杂的应用体系:支持单体、WEB应用、移动应用、桌面应用和无浏览器的应用; -
权限:对于Graph API数据来说,有大量的可以支持最小权限的大量权限设计,相当详细,后续连载也会详细讲解;第二个部分就是大B客户自己的API权限体系,可以在Portal门户网站进行自有API的用户添加访问,另外配合应用可以做更细粒度的API级访问权限;第三个就是权限授予,用户在访问高权限API的时候,需要管理员进行权限授予,减少权限滥用; -
访问逻辑:在MIP系统中有两个模式,一个是Client_ID+Client_Secrets的访问模式,通过这种访问模式可以直接以对应的应用身份来访问数据,这种访问方式的安全性还是相对较低的,一旦获取到了Client_ID和Client_Secrets就可以进行自有API和Graph API的数据访问,当然还是需要看具体的权限设计;另外一个就是使用OAuth2的登录方式,通过登录之后才可以访问对应的API,例如Office365 Outlook或者其他安全要求比较高的API,都需通过用户身份的访问才可以获取对应的数据例如邮件、文档等; -
MIP产品发展路径
-
登录生态:从2018年到2022年最新支持的登录生态列表包括IBM OpenPages、OneTrust Privacy Management Software、Dealpath、IriusRisk Federated Directory、Fidelity NetBenefits、Github、Twitter、Boxcryptor、CylancePROTECT、Wrike、SignalFx、Assistant by FirstAgenda、YardiOne、Vtiger CRM、inwink、Amplitude、Spacio、ContractWorks、Bersin、Mercell、Trisotech Digital Enterprise Server、Qumu Cloud、Criterion HCM、FiscalNote、Secret Server (On-Premises)、Dynamic Signal、mindWireless、OrgChart Now、Ziflow、AppNeta Performance、Monitor、Elium 、Fluxx Labs、Cisco Cloud、Shelf、SafetyNet、SAP(较多,不在过多罗列); -
B2B商业对商业认证模块: -
认证组织功能:B2B可以设置定向邀请和删除指定组织的加入功能; -
Guest用户访问App功能:通过SAML-based认证、 Integrated Windows Authentication (IWA) with Kerberos constrained delegation (KCD)认证让Guest用户访问对应的App功能; -
Guest用户密码管理:Guest用户进行OTP(One Time Passcode Authencation)认证方式,支持Guest临时访问需求,生成一次性访问密码;Guest用户生命周期管理,包括注册等;Guest用户权限限制; -
支持多云和混合云登录:Google、AWS登录方式; -
B2C商业对用户认证模块: -
统一登录:设置Azure Common通用登录模块,可以让所有Azure用户进行统一登录,提高登录体验; -
JIT:Just In Time登录,通过时间控制访问登录的用户; -
认证方式:手机号认证; -
账号安全加固: -
密码加固:禁止使用TOP100密码、以及100多万种TOP100密码的变换;同时支持Azure AD和Windows Server Active Directory两种认证模式;B2C和B2B的密码复杂度设置,包括简单、强壮等密码设计规则; -
Cookie加固:在AZURE AD APPLICATION PROXY APPS上设置HTTP-Only模块,减少客户端窃取Cookie风险; -
默认安全策略:默认安全策略所有Azure AD用户开启,包括MFA令牌认证方式默认支持Microsoft Authencator认证方式、管理员角色需开启MFA认证,其他用户无强制限制;LEGACY AUTHENCATION认证方式慢慢减少; -
威胁检测模块: -
登录异常:2018年开始微软就开始支持Azure Active Directory P2(最高等级)的异常登录检测功能;用户登录历史查看功能,用来发现尝试猜测密码的异常行为、所有成功和失败登录的IP地址记录、黑客访问了哪些App的记录等;可疑的邮件转发功能、出差行为异常判断; -
密码暴力破解:基于每小时和每天针对IP的异常暴力破解统计和告警功能; -
针对LEGACY AUTHENTICATION进行检测和告警; -
标识安全分:通过Identity Secure Score安全分来针对整体的身份水位进行评估; -
应用视角:针对每一个Enterprise App进行细粒度的应用级别登录错误事件、TOP登录失败占比排行榜; -
用户视角:了解每一个用户的登录历史、账号是否被入侵、是否是高风险用户等; -
API级:支持API级异常调用检测和告警; -
统一报告:高级过滤和排序、批量Action,如解除用户风险、确认受损害或安全的实体、风险状态:处于风险中、被驳回、被补救和确认已受损害; -
攻击模拟:模拟管理员身份进行黑客攻击模拟演练; -
恶意地址检测:包括恶意Malware相关的地址; -
风险调查:通过离线的Azure AD Threat Intelligence情报把调查到的对应情况,附加到风险报告的additionalInfo属性上,属于产品和威胁情报的结合; -
支持GraphAPI:支持GraphAPI相应的访问策略; -
PRT检测:跟Microsoft Defender进行结合,针对设备认证的PRT主刷新令牌进行威胁检测; -
日志采集模块: -
日志采集模块:包括登录的审计日志、自服务密码修改等审计日志、目录管理审计日志包括注册应用、用户管理等模块,并且开始逐步推进部署的地域;IP、浏览器版本、浏览器客户端、城市、地域、Legacy authentication和MFA认证信息; -
联动其他云产品:Azure日志模块支持Monitor、Azure Log Analytics产品,通过Azure Monitor产品可以设置Azure BLOB存储的轮转时效、跟SIEM集成、集成用户自身的应急响应平台、分析工具等;通过Azure Log Analytics产品把日志集中存储和分析以及可视化工作; -
管理员统一日志搜索:管理员进行统一审计日志的查询和搜索功能; -
扩展产品线:Azure Domain Service、MS Graph API活动日志; -
设备:设备相关的日志,创建和删除Passwordless认证日志,包括手机登录、FIDO2 key日志、Windows Hello认证以及注册删除设备日志; -
权限管理模块: -
权限:针对权限管理中的审计日志功能开启对应的非管理员访问日志审计权限;自定义权限角色管理;Global Administrator只读权限管理;Hybrid Identity认证模式包括PHS、PTA、ADFS配置角色; -
应用权限:细粒度的应用权限,不再使用Global Administrator来进行授权;细分权限包括应用管理员、云应用管理员、应用开发、应用注册Owner、企业用户Owner、设备管理角色; -
权限管理PIM产品(Privileged Identity Management):周报功能,包含角色分配用户数、PIM覆盖的用户数、PIM未覆盖的用户数等权限管理收敛工作;资源所分配的权限的导出功能;PIM支持更多产品的ABAC访问属性(例如Azure Storage); -
组管理:恢复误删除的组;组别名设置功能;导出组用户名单;组用户生命周期管理;动态组策略分配相应的用户 -
权限访问包:权限管理包包括生态和用户的访问周期、AzureAD和Office365的用户列表、企业应用角色分配等;权限包过期功能; -
合规: -
PCI-DSS:取消TLS 1.0支持TLS1.2和TLS1.3; -
身份治理: -
访问治理:通过Access reviews来进行季度和月度进行Reviews; -
标准认证协议模块: -
SAML2:SAML2增加了对应的EmployeeID属性,用来管理B2B(企业到企业)登录的员工身份标识字段;通过测试模块直接登录SAML2认证协议的用户登录接口;SAML2认证Token加密功能;支持基于On-Permises的SAML2应用用户认证;SAML2增加更多属性,包括用户、组等; -
JWT:开始支持自定义的JWT和SAML2认证协议,让用户可以携带自己的私钥做认证;JIT支持资源组,包括VM、App Services等; -
OAuth2:支持Windows/Mac客户端或者iOS/Android客户端的PKCE安全认证模式,防止本地保存对应的私钥; -
LEGACY AUTHENTICATIONS:
-
Pass-through Authentication (PTA)认证协议:支持Office登录、Exchange、Skype for Business以及Apple Device Enrollment Program (Apple DEP);
-
WS-FED:B2B和B2C支持WS-FED认证;
-
SCIM:支持SCIM认证方式;
-
OpenID:OpenID认证方式支持GraphAPI、B2B和B2C;
-
诊断:
-
RequestID:通过RequestID来进行Azure AD支持的登录异常和需要后台人员支持的Support服务;
-
登录诊断:包括登录过程、MFA认证、条件访问、登录上下文等进行针对,发现潜在问题;
-
应用配置针对:Enterprise企业应用配置诊断;企业应用事件日志;不正确的凭证事件;
-
设备: -
Intune:支持Office等Native登录认证模式,主要是跟终端产品打通身份认证; -
操作系统:Bitlocker审计日志,通过Azure AD访问Bitlocker密钥进行审计和告警; -
Conditional Access(条件访问):
-
设备:支持Intune Managed Browser SSO认证的条件访问,等于终端上有一个信息采集器,用来让条件访问做对应的策略;终端访问Apps应用并没有Intune License信息,Intune保护设施未开启禁止访问Apps;是否是托管设备;
-
细粒度策略:针对细粒度的策略查看对应的告警和命中情况;支持服务应用访问条件访问、支持Azure Portal、SharePoint等;
-
应用:应用力度的条件访问,逐步扩大支撑范围;包括Exchange ActiveSync评估登录的地址,包括城市、地域或者IP地址,评估登录风险、设备平台登录访问条件、浏览器客户端以及浏览器版本、用户成功和失败登录记录;
-
浏览器:通过Edge、Firefox(Win10/11、Windows Server 2019)等浏览器设置对应的访问策略;
-
网络:SSPR密码重置和MFA支持可信网络策略、是否是托管设备;
-
用户:用户登录综合评估是低风险;
-
仅报告模式:针对条件访问规则,仅仅是报告和观察状态,并指定可以不进行策略执行动作;
-
命令行(Powershell模块):简化管理,使用Powershell来进行对应的管理;支持GraphAPI、Azure AD管理等命令行;
-
MFA:
-
统一控制台:针对手机号、邮箱、手机应用MFA、SSRP密码重置控制台等进行统一的界面,提高用户体验;
-
密码重置:通过管理员开启MFA可以重置密码功能,让用户进行密码重置通过MFA增强密码重置水位;
-
认证恢复:丢失MFA之后,可以通过临时访问功能,设置新的MFA;
-
数据确权:Azure AD认证数据可以符合GDPR等数据存储的要求,包括Gov政务云、Azure公有云、情报云等都属于独立的数据存储,有独有的数据存储安全需求;
-
Passwordless认证模块:
-
FIDO2:支持FIDO2 Passwordless认证模块;支持Hybrid混合认证方式,包括线下的WIN10加入Azure AD域设备认证;
-
SMS手机号登录:通过SMS手机号进行登录,而不是用密码的Passwordless方式;
-
软令牌:通过Microsoft Authencator进行软令牌认证;
-
SmartCard:认证高等级的认证,采用SmartCard硬件令牌进行认证;
-
Windows Hello:通过Windows Hello进行身份认证;
-
自动认证:IOS和Android设备自动填充登录密码;
-
零信任:
-
持续验证:持续访问评估(Continuos Access Evaluation)Azure AD安全状态(例如Azure用户删除)之后近乎实时状态禁止通过Token访问相关应用;
-
GraphAPI:
-
目录功能:Graph API目录API支持统计(Count)、搜索(Search)、Filter(过滤)、Sort(排序)等功能;
-
认证支持:GraphAPI支持通过配置SAML2来进行应用身份认证;
-
扩展更多目录:包括风险、用户、组、应用、服务、终端等;
-
更新属性:管理员可以更新用户的对应属性,包括MFA手机、邮箱、姓名等等属性;
-
减少隐私:GraphAPI对应的Secret Token信息泄露的问题进行更新;GraphAPI设置Password Secret认证方式,并且无法看到明文;
-
支持设备认证:GraphAPI支持MDM、MAM配置等;
-
权限设计:针对GraphAPI授予过多的访问权限进行限制;
-
混合标识身份认证:
-
身份同步:Azure AD Connect Cloud SYNC同步策略;
-
应用加固策略:
-
覆盖Workload身份:针对应用、服务、托管的身份来进行检测、调查、削减对应的身份相关的风险;条件访问策略可以同样适用在Workload身份上;
-
总结:MIP不断通过几个维度来扩展身份的安全性,逐步完整更大的身份安全版图:
-
用户认证维度:不断扩充认证协议,包括SAML2、OAuth2、OpenID等协议;
-
设备认证维度:Intune产品线就是不断支持更多操作系统,包括安卓、IOS、Windows、MacOS等;例如微软的KPI就是通过Intune 100%的托管设备;
-
不断增强MFA能力:包括Microsoft软令牌、手机认证、FIDO2认证等;
-
不断的权限管理:针对权限管理进行细粒度的管控;
-
条件访问:针对应用、用户、操作系统、浏览器等进行访问条件的风控策略,针对异常攻击进行检测;
-
增强威胁检测能力:针对前面覆盖的能力进行大量日志采集和分析以及跟第三方联动集成SIEM;
-
生态增强:下面会专门针对Intune生态、Passwordless生态维度展开讲一下微软身份的生态体系;
-
威胁情报和MSSP服务兜底:通过微软威胁情报体系和对应的MSSP专家服务不断构建身份安全的托管服务、威胁调查服务,来提升用户的安全水位;
-
MIP生态发展 -
非常多的大型ToB的客户针对设备管理,都有自己的成熟的管理方案,再上云的过程中,也许不会选择到Intune微软的设备管理套件,例如TOP 500强等很多企业都会采用以下的生成的MDM/MAM管理平台,包括BlackBerry UEM、Citrix Workspace device compliance、IBM MaaS360、JAMF Pro、MobileIron Device Compliance Cloud、MobileIron Device Compliance On-prem、SOTI MobiControl、VMware Workspace ONE UEM (formerly AirWatch)等,微软Intune为了更好的支持这些客户,扩展商业边界不断的侵蚀更多的大B客户;
-
PasswordLess生态: -
FIDO2生态:如下图所示在单点FIDO2身份认证领域,跟众多行业进行联动,包括Yubico(Amazon Midway、Google MOMA都是采用了Yubico的认证方式)、飞天诚信、TrustKey等FIDO2事实上厂商进行深度合作,逐步扩展PasswordLess认证边界,扩大支持客户群体;
-
混合云/多云支持:支持AWS、Azure、Google等,微软的战略一直在走混合云和多云战略,保持不断扩展云的业务边界; -
支持登录的应用生态:很明显国外SaaS化相当普及,微软也跟众多的知名SaaS展开合作,包括Salesforces、ServiceNow、Dropbox、Box、Zoom、Atlassian等,其实跟这些厂商的生态合作,主要是对应的TOP SaaS厂商支持微软系的Identity身份认证配置,如果一家大型ToB企业购买了SaaS厂商的产品,而不希望在维护另外一套身份账号体系,借由微软Identity进行了身份的统一登录,访问SaaS的时候输入企业的ToB身份标识例如邮箱,就会跳转回MIP登录平台进行认证、风险管控、MFA、威胁检测发现等;
-
MIP技术细节-GraphAPI认证部分 -
Graph API Token认证:微软最核心的身份认证API就是Graph API,通过Graph API可以有效的管理整个Azure AD的各种信息,一旦Graph API被攻破,那就等于极其严重的影响云平台信任级的史诗漏洞。下面针对Graph API验证Token的逻辑进行详细技术层面分析: -
提交ClientID和ClientSecrets来获取对应的访问Token凭据;
POST https://login.microsoftonline.com/{TenantID}/oauth2/v2.0/token
Content-Type : application/x-www-form-urlencoded
scope=https://graph.microsoft.com/.default&grant_type=client_credentials&client_id=535fb089-9ff3-47b6-9bfb-4f1264799865&client_secret=qWgdYAmab0YSkuL1qKv5bPX
-
Graph API Token令牌认证逻辑:下面从代码层面看一下是如何验证Token令牌的(不得不佩服微软代码的可读性还是相当高的),第一步是开始进行认证请求同步验证;
private async Task<IPrincipal> AuthenticateRequestAsync(HttpContext context)
{
IPrincipal user = context.get_User();
if (user == null)
{
NameValueCollection headers = context.get_Request().get_Headers();
if (headers != null)
{
string[] authHeaderValues = headers.GetValues("Authorization");
if (!authHeaderValues.IsNullOrEmpty())
{
string token = GetEncryptedBearerToken(authHeaderValues);
if (!string.IsNullOrEmpty(token))
{
if (config.ForwardDecryptedAuthorizationTokens)
{
headers["Authorization"] = "Bearer " + token;
}
}
else
{
token = GetBearerToken(authHeaderValues);
}
if (!string.IsNullOrEmpty(token))
{
try
{
JwtValidater validater = new JwtValidater(config, ValidationParametersCache, tracer);
user = await validater.ValidateToken(token, (AadApplicationId appIdFromToken) => allowedApplicationIds.Count < 1 || pathsThatAllowAllApplications.Contains(context.get_Request().get_AppRelativeCurrentExecutionFilePath()) || allowedApplicationIds.Contains(appIdFromToken)).ConfigureAwait(continueOnCapturedContext: false);
}
catch (ArgumentException e)
{
string message3 = string.Format(CultureInfo.InvariantCulture, "Failed to validate token. AuthorizationHeader={0}, Exception={1}", token, e);
tracer.Warning(message3, "AuthenticateRequestAsync", "X:\bt\1037330\repo\src\Security\Aad\Core\AadJwtAuthModule.cs", 155);
}
catch (Exception genericException)
{
string message2 = string.Format(CultureInfo.InvariantCulture, "Failed to validate token. AuthorizationHeader={0}", token);
tracer.UnexpectedException(genericException, message2, "AuthenticateRequestAsync", "X:\bt\1037330\repo\src\Security\Aad\Core\AadJwtAuthModule.cs", 165);
throw;
}
if (user is ClaimsPrincipal claimsPrincipal)
{
user = new ClaimsPrincipal(new ExtensionAadIdentity(claimsPrincipal, tracer, config));
}
else if (user != null)
{
string message = string.Format(CultureInfo.InvariantCulture, "Unable to authorize, the identity constructed from the auth header is not supported. typeof(user)={0}, AuthorizationHeader={1}", user.GetType(), token);
tracer.Warning(message, "AuthenticateRequestAsync", "X:\bt\1037330\repo\src\Security\Aad\Core\AadJwtAuthModule.cs", 184);
}
}
}
}
user = user ?? AnonymousUser;
context.set_User(user);
}
if ((user.Identity == null || !user.Identity!.IsAuthenticated) && !UrlAuthorizationModule.CheckUrlAccessForPrincipal(context.get_Request().get_Path(), user, context.get_Request().get_RequestType()))
{
context.get_Response().set_StatusCode(401);
}
return context.get_User();
}
Authorization的Headers头,这里看过前面MIP身份认证的大逻辑的读者就会比较清晰了,这里应用的访问身份是有两个的,一个是用户的身份,一个是应用的身份,分别对应人访问和机器访问逻辑。所以顺利成章,如果并未以用户身份访问Graph API的话,就开始下一步的Token解析逻辑。
private string GetBearerToken(IEnumerable<string> authHeaderValues)
{
return GetAuthHeaderToken("Bearer ", authHeaderValues);
}
private string GetAuthHeaderToken(string prefix, IEnumerable<string> authHeaderValues)
{
IEnumerable<string> bearerTokenValues = from value in authHeaderValues
where value?.StartsWith(prefix, StringComparison.Ordinal) ?? false
select value.Substring(prefix.Length);
return bearerTokenValues.FirstOrDefault();
}
3、继续跟进到比较核心的代码逻辑验证Token的有效性,这两行代码的重要性非常高的,等于整个微软Graph API的认证逻辑绝对是核心中的核心代码;继续跟进ValidateToken这个函数;
JwtValidater validater = new JwtValidater(config, ValidationParametersCache, tracer);
user = await validater.ValidateToken(token, (AadApplicationId appIdFromToken) => allowedApplicationIds.Count < 1 || pathsThatAllowAllApplications.Contains(context.get_Request().get_AppRelativeCurrentExecutionFilePath()) || allowedApplicationIds.Contains(appIdFromToken)).ConfigureAwait(continueOnCapturedContext: false);
4、ValidateToken有几个参数输入Token就是Client_ID和Client_Secrets认证后的令牌和需要验证的AppId也就是后来说的aud验证部分;
public async Task<IPrincipal> ValidateToken(string token, Func<AadApplicationId, bool> validateAppId)
{
if (string.IsNullOrEmpty(token))
{
return null;
}
ClaimsPrincipal claimsPrincipal = null;
try
{
JwtValidationParameters validationParams = await GetCachedValidationParameters().ConfigureAwait(continueOnCapturedContext: false);
if (validationParams != null)
{
IEnumerable<string> allowedAudiences = from audience in config.AllowedAudiences.MapNullToEmpty()
where audience != null
select audience;
if (token.StartsWith("Bearer ", StringComparison.Ordinal))
{
token = token.Substring("Bearer ".Length);
}
claimsPrincipal = GetClaimsPrincipalFromToken(token, config.TenantId, validationParams.Issuer, allowedAudiences, validationParams.SigningKeys, validateAppId);
}
}
catch (ArgumentException e4)
{
tracer.Information("Unable to get claims principal from access token: {0}".FormatInvariant(e4), "ValidateToken", "X:\bt\1037330\repo\src\Security\Aad\Core\AadJwtValidater.cs", 109);
}
catch (SecurityTokenExpiredException e3)
{
bool isShortLivedToken = false;
try
{
isShortLivedToken = IsShortLivedToken(token);
}
catch (Exception)
{
}
if (isShortLivedToken)
{
tracer.Warning(string.Format(CultureInfo.InvariantCulture, "Unable to get claims principal from expired short lived access token: {0}", e3), "ValidateToken", "X:\bt\1037330\repo\src\Security\Aad\Core\AadJwtValidater.cs", 126);
}
else
{
tracer.Information(string.Format(CultureInfo.InvariantCulture, "Unable to get claims principal from expired access token: {0}", e3), "ValidateToken", "X:\bt\1037330\repo\src\Security\Aad\Core\AadJwtValidater.cs", 130);
}
}
catch (SecurityTokenValidationException e2)
{
tracer.Warning(string.Format(CultureInfo.InvariantCulture, "Unable to get claims principal from access token: {0}", e2), "ValidateToken", "X:\bt\1037330\repo\src\Security\Aad\Core\AadJwtValidater.cs", 137);
}
catch (SignatureVerificationFailedException e)
{
tracer.Information(string.Format(CultureInfo.InvariantCulture, "Unable to get claims principal from access token: {0}", e), "ValidateToken", "X:\bt\1037330\repo\src\Security\Aad\Core\AadJwtValidater.cs", 147);
}
return claimsPrincipal;
}
private ClaimsPrincipal GetClaimsPrincipalFromToken(string token, string issuerTenantId, string issuer, IEnumerable<string> allowedAudiences, IEnumerable<SecurityKey> signingKeys, Func<AadApplicationId, bool> validateAppId)
{
bool shouldValidateIssuer = !string.Equals(issuerTenantId, "common", StringComparison.OrdinalIgnoreCase);
TokenValidationParameters validationParameters = new TokenValidationParameters
{
ValidAudiences = allowedAudiences,
ValidateIssuer = shouldValidateIssuer,
ValidIssuer = issuer,
ClockSkew = config.MaxValidationClockSkewInterval,
IssuerSigningKeys = signingKeys
};
SecurityToken validatedToken;
ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
if (validatedToken != null)
{
if (!ValidateApplicationId(validatedToken as JwtSecurityToken, validateAppId))
{
return null;
}
return claimsPrincipal;
}
return claimsPrincipal;
}
6、继续跟进tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
public override ClaimsPrincipal ValidateToken(string token, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
{
if (string.IsNullOrWhiteSpace(token))
{
throw LogHelper.LogArgumentNullException("token");
}
if (validationParameters == null)
{
throw LogHelper.LogArgumentNullException("validationParameters");
}
if (token.Length > MaximumTokenSizeInBytes)
{
throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant("IDX10209: token has length: '{0}' which is larger than the MaximumTokenSizeInBytes: '{1}'.", token.Length, MaximumTokenSizeInBytes)));
}
string[] array = token.Split(new char[1] { '.' }, 6);
if (array.Length != 3 && array.Length != 5)
{
throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant("IDX12741: JWT: '{0}' must have three segments (JWS) or five segments (JWE).", token)));
}
if (array.Length == 5)
{
JwtSecurityToken jwtSecurityToken = ReadJwtToken(token);
string token2 = DecryptToken(jwtSecurityToken, validationParameters);
JwtSecurityToken jwtToken = (jwtSecurityToken.InnerToken = ValidateSignature(token2, validationParameters));
validatedToken = jwtSecurityToken;
return ValidateTokenPayload(jwtToken, validationParameters);
}
validatedToken = ValidateSignature(token, validationParameters);
return ValidateTokenPayload(validatedToken as JwtSecurityToken, validationParameters);
}
7、继续跟进jwtSecurityToken.Decode函数
internal void Decode(string[] tokenParts, string rawData)
{
LogHelper.LogInformation("IDX12716: Decoding token: '{0}' into header, payload and signature.", rawData);
try
{
Header = JwtHeader.Base64UrlDeserialize(tokenParts[0]);
}
catch (Exception innerException)
{
throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant("IDX12729: Unable to decode the header '{0}' as Base64Url encoded string. jwtEncodedString: '{1}'.", tokenParts[0], rawData), innerException));
}
if (tokenParts.Length == 5)
{
DecodeJwe(tokenParts);
}
else
{
DecodeJws(tokenParts);
}
RawData = rawData;
}
8、从上面代码可以看到Token分成了Jwe和Jws两种模式,看一下DecodeJwe和DecodeJws的区别;Jwe是比Jws的安全等级更高,Jwe可以针对JWT Payload部分进行加密;
private void DecodeJwe(string[] tokenParts)
{
RawHeader = tokenParts[0];
RawEncryptedKey = tokenParts[1];
RawInitializationVector = tokenParts[2];
RawCiphertext = tokenParts[3];
RawAuthenticationTag = tokenParts[4];
}
DecodeJws代码,针对JWT Header、Payload和签名进行解析;这里面的RawHeader里面包含了如何验证令牌,以及令牌的类型和相关的加密算法,例如{“typ”:”JWT”,”alg”:”RS256″,”kid”:”i6lGk3FZzxRcUb2C3nEQ7syHJlY”}其中typ是类型JWT,ALG算法是RS265,KID是公钥验签的公钥签名,RawPayload就是相关的核心跟用户相关的参数信息,RawSignature就是JWT的签名部分,用来防篡改等操作;
private void DecodeJws(string[] tokenParts)
{
if (Header.Cty != null)
{
LogHelper.LogVerbose(LogHelper.FormatInvariant("IDX12738: Header.Cty != null, assuming JWS. Cty: '{0}'.", Header.Cty));
}
try
{
Payload = JwtPayload.Base64UrlDeserialize(tokenParts[1]);
}
catch (Exception innerException)
{
throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant("IDX12723: Unable to decode the payload '{0}' as Base64Url encoded string. jwtEncodedString: '{1}'.", tokenParts[1], RawData), innerException));
}
RawHeader = tokenParts[0];
RawPayload = tokenParts[1];
RawSignature = tokenParts[2];
}
9、继续跟验证JWT Token签名部分的核心逻辑,前面讲到了JWT的RawHeader获取部分,有Kid这个参数,Kid参数就是指定使用哪个JWT的公钥验签,首先获取IssuerSigningKey核心代码是enumerable = validationParameters.IssuerSigningKeyResolver(token, jwtSecurityToken, jwtSecurityToken.Header.Kid, validationParameters);,然后通过ValidateSignature(bytes, signature, item, jwtSecurityToken.Header.Alg, validationParameters)进行验签,Kid的获取还是需要讲透的,首先微软在JWT的OpenID配置Endpoint可以获取对应的信息,https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration访问获取到”jwks_uri”:”https://login.microsoftonline.com/common/discovery/v2.0/keys”,继续访问https://login.microsoftonline.com/common/discovery/v2.0/keys获取到全部的公钥,根据观察和分析这里微软有一个很变态的操作,就是24小时会进行公钥配置的轮转。继续讲验证签名的逻辑,通过Kid来获取对应的公钥验证签名,例如下图的nOo3ZDrODXEK1jKWhXslHR_KXEg来验签。
[{"kty":"RSA","use":"sig","kid":"nOo3ZDrODXEK1jKWhXslHR_KXEg","x5t":"nOo3ZDrODXEK1jKWhXslHR_KXEg","n":"oaLLT9hkcSj2tGfZsjbu7Xz1Krs0qEicXPmEsJKOBQHauZ_kRM1HdEkgOJbUznUspE6xOuOSXjlzErqBxXAu4SCvcvVOCYG2v9G3-uIrLF5dstD0sYHBo1VomtKxzF90Vslrkn6rNQgUGIWgvuQTxm1uRklYFPEcTIRw0LnYknzJ06GC9ljKR617wABVrZNkBuDgQKj37qcyxoaxIGdxEcmVFZXJyrxDgdXh9owRmZn6LIJlGjZ9m59emfuwnBnsIQG7DirJwe9SXrLXnexRQWqyzCdkYaOqkpKrsjuxUj2-MHX31FqsdpJJsOAvYXGOYBKJRjhGrGdONVrZdUdTBQ","e":"AQAB","x5c":["MIIDBTCCAe2gAwIBAgIQN33ROaIJ6bJBWDCxtmJEbjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIwMTIyMTIwNTAxN1oXDTI1MTIyMDIwNTAxN1owLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKGiy0/YZHEo9rRn2bI27u189Sq7NKhInFz5hLCSjgUB2rmf5ETNR3RJIDiW1M51LKROsTrjkl45cxK6gcVwLuEgr3L1TgmBtr/Rt/riKyxeXbLQ9LGBwaNVaJrSscxfdFbJa5J+qzUIFBiFoL7kE8ZtbkZJWBTxHEyEcNC52JJ8ydOhgvZYykete8AAVa2TZAbg4ECo9+6nMsaGsSBncRHJlRWVycq8Q4HV4faMEZmZ+iyCZRo2fZufXpn7sJwZ7CEBuw4qycHvUl6y153sUUFqsswnZGGjqpKSq7I7sVI9vjB199RarHaSSbDgL2FxjmASiUY4RqxnTjVa2XVHUwUCAwEAAaMhMB8wHQYDVR0OBBYEFI5mN5ftHloEDVNoIa8sQs7kJAeTMA0GCSqGSIb3DQEBCwUAA4IBAQBnaGnojxNgnV4+TCPZ9br4ox1nRn9tzY8b5pwKTW2McJTe0yEvrHyaItK8KbmeKJOBvASf+QwHkp+F2BAXzRiTl4Z+gNFQULPzsQWpmKlz6fIWhc7ksgpTkMK6AaTbwWYTfmpKnQw/KJm/6rboLDWYyKFpQcStu67RZ+aRvQz68Ev2ga5JsXlcOJ3gP/lE5WC1S0rjfabzdMOGP8qZQhXk4wBOgtFBaisDnbjV5pcIrjRPlhoCxvKgC/290nZ9/DLBH3TbHk8xwHXeBAnAjyAqOZij92uksAv7ZLq4MODcnQshVINXwsYshG1pQqOLwMertNaY5WtrubMRku44Dw7R"],"issuer":"https://login.microsoftonline.com/{tenantid}/v2.0"}
protected virtual JwtSecurityToken ValidateSignature(string token, TokenValidationParameters validationParameters)
{
if (string.IsNullOrWhiteSpace(token))
{
throw LogHelper.LogArgumentNullException("token");
}
if (validationParameters == null)
{
throw LogHelper.LogArgumentNullException("validationParameters");
}
if (validationParameters.SignatureValidator != null)
{
SecurityToken securityToken = validationParameters.SignatureValidator(token, validationParameters);
if (securityToken == null)
{
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant("IDX10505: Signature validation failed. The user defined 'Delegate' specified on TokenValidationParameters returned null when validating token: '{0}'.", token)));
}
if (!(securityToken is JwtSecurityToken result))
{
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant("IDX10506: Signature validation failed. The user defined 'Delegate' specified on TokenValidationParameters did not return a '{0}', but returned a '{1}' when validating token: '{2}'.", typeof(JwtSecurityToken), securityToken.GetType(), token)));
}
return result;
}
JwtSecurityToken jwtSecurityToken = null;
if (validationParameters.TokenReader != null)
{
SecurityToken securityToken2 = validationParameters.TokenReader(token, validationParameters);
if (securityToken2 == null)
{
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant("IDX10510: Signature validation failed. The user defined 'Delegate' specified in TokenValidationParameters returned null when reading token: '{0}'.", token)));
}
jwtSecurityToken = securityToken2 as JwtSecurityToken;
if (jwtSecurityToken == null)
{
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant("IDX10509: Signature validation failed. The user defined 'Delegate' specified in TokenValidationParameters did not return a '{0}', but returned a '{1}' when reading token: '{2}'.", typeof(JwtSecurityToken), securityToken2.GetType(), token)));
}
}
else
{
jwtSecurityToken = ReadJwtToken(token);
}
byte[] bytes = Encoding.UTF8.GetBytes(jwtSecurityToken.RawHeader + "." + jwtSecurityToken.RawPayload);
if (string.IsNullOrEmpty(jwtSecurityToken.RawSignature))
{
if (validationParameters.RequireSignedTokens)
{
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant("IDX10504: Unable to validate signature, token does not have a signature: '{0}'.", token)));
}
return jwtSecurityToken;
}
bool flag = false;
IEnumerable<SecurityKey> enumerable = null;
if (validationParameters.IssuerSigningKeyResolver != null)
{
enumerable = validationParameters.IssuerSigningKeyResolver(token, jwtSecurityToken, jwtSecurityToken.Header.Kid, validationParameters);
}
else
{
SecurityKey securityKey = ResolveIssuerSigningKey(token, jwtSecurityToken, validationParameters);
if (securityKey != null)
{
flag = true;
enumerable = new List<SecurityKey> { securityKey };
}
}
if (enumerable == null)
{
enumerable = GetAllSigningKeys(token, jwtSecurityToken, jwtSecurityToken.Header.Kid, validationParameters);
}
StringBuilder stringBuilder = new StringBuilder();
StringBuilder stringBuilder2 = new StringBuilder();
bool flag2 = !string.IsNullOrEmpty(jwtSecurityToken.Header.Kid);
byte[] signature;
try
{
signature = Base64UrlEncoder.DecodeBytes(jwtSecurityToken.RawSignature);
}
catch (FormatException innerException)
{
throw new SecurityTokenInvalidSignatureException("IDX10508: Signature validation failed. Signature is improperly formatted.", innerException);
}
foreach (SecurityKey item in enumerable)
{
try
{
if (ValidateSignature(bytes, signature, item, jwtSecurityToken.Header.Alg, validationParameters))
{
LogHelper.LogInformation("IDX10242: Security token: '{0}' has a valid signature.", token);
jwtSecurityToken.SigningKey = item;
return jwtSecurityToken;
}
}
catch (Exception ex)
{
stringBuilder.AppendLine(ex.ToString());
}
if (item != null)
{
stringBuilder2.AppendLine(item.ToString() + " , KeyId: " + item.KeyId);
if (flag2 && !flag && item.KeyId != null)
{
flag = jwtSecurityToken.Header.Kid.Equals(item.KeyId, (item is X509SecurityKey) ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
}
}
if (flag2)
{
if (flag)
{
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant("IDX10511: Signature validation failed. Keys tried: '{0}'. nkid: '{1}'. nExceptions caught:n '{2}'.ntoken: '{3}'.", stringBuilder2, jwtSecurityToken.Header.Kid, stringBuilder, jwtSecurityToken)));
}
throw LogHelper.LogExceptionMessage(new SecurityTokenSignatureKeyNotFoundException(LogHelper.FormatInvariant("IDX10501: Signature validation failed. Unable to match keys: nkid: '{0}', ntoken: '{1}'.", jwtSecurityToken.Header.Kid, jwtSecurityToken)));
}
if (stringBuilder2.Length > 0)
{
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant("IDX10503: Signature validation failed. Keys tried: '{0}'.nExceptions caught:n '{1}'.ntoken: '{2}'.", stringBuilder2, stringBuilder, jwtSecurityToken)));
}
throw LogHelper.LogExceptionMessage(new SecurityTokenSignatureKeyNotFoundException("IDX10500: Signature validation failed. No security keys were provided to validate the signature."));
}
10、验签完成了,验证签名通过之后如何来验证用户和权限,整个访问的逻辑是首先通过aud参数来验证是否有权限访问Graph API这个应用,每一个Graph API都有对应的ID来标识应用;通过授予的令牌中的oid、roles、wids来验证调用Token的oid对应的用户是否有对应的权限和对应的角色roles,对应的权限和对应的角色可以授予用户能访问什么样的数据,最终完成整个Graph API的调用的验证。当然中间还有对应的签名过期时间、login.microsoftonline.com Endpoint发布的验证Nonce的有效性等,这里不做过多赘述。
11、分析完Graph API的整个认证逻辑,再来分析一下整个Graph API的攻击面,目前Graph API最大的攻击面就是可以直接通过获取Client_ID和Client_Secrets的方式来进行应用层的访问,直接获取敏感数据。另外Graph API中也存在了大量的敏感数据,一旦权限设计不合理,包括组织架构、人员信息、设备信息、组信息、邮件信息、SharePoint文件信息等,都可以通过Graph API Token的方式进行获取。另外一个攻击面,就是整个Graph API设计的最核心就是依靠私钥Keys来做的JWT的验证逻辑,一旦私钥被获取,那对平台的影响是毁灭的(当然概率较小),笔者目前也正在分析私钥的存储方式,通过公开渠道来获取对应的设计思想,后期如找到对应的文献笔者一定第一时间分享。还有一个攻击面就是可以用来做各种猥琐的后门,通过设计恶意的应用加上较大的权限来持续保持权限;还可以通过在原有的Application和ServicePriciple上增加密码认证,通过Graph API的Application API的passwordCredential来通过密码认证来持续保持权限;同时也可以导入证书,通过Application API的keyCredential方式进行持续权限的保持方式(历史上Application API出现过泄露私钥的漏洞,漏洞编号https://msrc-blog.microsoft.com/2021/11/17/guidance-for-azure-active-directory-ad-keycredential-property-information-disclosure-in-application-and-service-principal-apis/)这个漏洞的影响还是非常巨大的。
关于微软身份Token参数的扩展阅读:
https://docs.microsoft.com/zh-cn/azure/active-directory/develop/id-tokens
原文始发于微信公众号(鸟哥谈安全):零信任安全架构连载一:复杂场景下的零信任安全设计详解