OAuth and OIDC
OAuth 2.0
OAuth 是一个关于授权(Authorization)的开放网络标准,在全世界得到了广泛的应用,目前的版本是2.0
名词解释
Third-party application:第三方应用程序,本文中又称"客户端"(client)。
HTTP Service:HTTP服务提供商,本文中简称"服务提供商"。
Resource Owner:资源所S有者,本文中又称"用户"(user)。
User Agent:用户代理,本文中就是指浏览器。
Authorization server:认证服务器,即服务提供商专门用来处理认证的服务器。
Resource server:资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。
OAuth在"客户端"与"服务提供商"之间,设置了一个授权层(authorization layer)。“客户端"不能直接登录"服务提供商”,只能登录授权层,以此将用户与客户端区分开来。"客户端"登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。
"客户端"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料。
授权流程
用户打开客户端以后,客户端要求用户给予授权。
用户同意给予客户端授权。
客户端使用上一步获得的授权,向认证服务器申请令牌。
认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
客户端使用令牌,向资源服务器申请获取资源。
资源服务器确认令牌无误,同意向客户端开放资源。
这几步中的关键就是客户端如何获取授权,OAuth2.0定义了四种不同的获取授权的方式。
获取授权的四种方式
客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式。
- 授权码模式(authorization code)
- 简化模式(implicit)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
授权码模式
(A)用户访问客户端,后者将前者导向认证服务器。
(B)用户选择是否给予客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
(D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
A步骤中,客户端申请认证的URI,包含以下参数:
- response_type:表示授权类型,必选项,此处的值固定为"code"
- client_id:表示客户端的ID,必选项
- redirect_uri:表示重定向URI,可选项
- scope:表示申请的权限范围,可选项
- state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
C步骤中,服务器回应客户端的URI,包含以下参数:
- code:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。
- state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
D步骤中,客户端向认证服务器申请令牌的HTTP请求,包含以下参数:
- grant_type:表示使用的授权模式,必选项,此处的值固定为"authorization_code"。
- code:表示上一步获得的授权码,必选项。
- redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。
- client_id:表示客户端ID,必选项。
E步骤中,认证服务器发送的HTTP回复,包含以下参数:
- access_token:表示访问令牌,必选项。
- token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
- expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
- refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。
- scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。
简化模式
简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。
(A)客户端将用户导向认证服务器。
(B)用户决定是否给于客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端指定的"重定向URI",并在URI的Hash部分包含了访问令牌。
(D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。
(E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。
(F)浏览器执行上一步获得的脚本,提取出令牌。
(G)浏览器将令牌发给客户端。
密码模式
密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。
在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。
(A)用户向客户端提供用户名和密码。
(B)客户端将用户名和密码发给认证服务器,向后者请求令牌。
(C)认证服务器确认无误后,向客户端提供访问令牌。
B步骤中,客户端发出的HTTP请求,包含以下参数:
- grant_type:表示授权类型,此处的值固定为"password",必选项。
- username:表示用户名,必选项。
- password:表示用户的密码,必选项。
- scope:表示权限范围,可选项。
客户端模式
客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。
更新令牌
如果用户访问的时候,客户端的"访问令牌"已经过期,则需要使用"更新令牌"申请一个新的访问令牌。
客户端发出更新令牌的HTTP请求,包含以下参数:
- granttype:表示使用的授权模式,此处的值固定为"refreshtoken",必选项。
- refresh_token:表示早前收到的更新令牌,必选项。
- scope:表示申请的授权范围,不可以超出上一次申请的范围,如果省略该参数,则表示与上一次一致。
OIDC
OpenID Connect是基于OAuth 2.0规范族的可互操作的身份验证协议。它使用简单的REST / JSON消息流来实现,和之前任何一种身份认证协议相比,开发者可以轻松集成。
OpenID Connect允许开发者验证跨网站和应用的用户,而无需拥有和管理密码文件。OpenID Connect允许所有类型的客户,包括基于浏览器的JavaScript和本机移动应用程序,启动登录流动和接收可验证断言对登录用户的身份。
简要而言,OIDC是一种安全机制,用于应用连接到身份认证服务器(Identity Service)获取用户信息,并将这些信息以安全可靠的方法返回给应用。
区别
OpenID是Authentication,即认证,对用户的身份进行认证,判断其身份是否有效,也就是让网站知道“你是你所声称的那个用户”;
OAuth是Authorization,即授权,在已知用户身份合法的情况下,经用户授权来允许某些操作,也就是让网站知道“你能被允许做那些事情”。
由此可知,授权要在认证之后进行,只有确定用户身份只有才能授权。
OpenID Connect是“认证”和“授权”的结合,因为其基于OAuth协议,所以OpenID-Connect协议中也包含了client_id、client_secret还有redirect_uri等字段标识。这些信息被保存在“身份认证服务器”,以确保特定的客户端收到的信息只来自于合法的应用平台。这样做是目的是为了防止client_id泄露而造成的恶意网站发起的OIDC流程。
流程简介
OAuth2提供了Access Token来解决授权第三方客户端访问受保护资源的问题;相似的,OIDC在这个基础上提供了ID Token来解决第三方客户端标识用户身份认证的问题。OIDC的核心在于在OAuth2的授权流程中,一并提供用户的身份认证信息(ID-Token)给到第三方客户端,ID-Token使用JWT格式来包装,得益于 JWT的自包含性,紧凑性以及防篡改机制,使得ID-Token可以安全的传递给第三方客户端程序并且容易被验证。应有服务器,在验证ID-Token正确只有,使用Access-Token向UserInfo的接口换取用户的更多的信息。
有上述可知,OIDC是遵循OAuth协议流程,在申请Access-Token的同时,也返回了ID-Token来验证用户身份。
术语
EU:End User,用户。
RP:Relying Party ,用来代指OAuth2中的受信任的客户端,身份认证和授权信息的消费方;
OP:OpenID Provider,有能力提供EU身份认证的服务方(比如OAuth2中的授权服务),用来为RP提供EU的身份认证信息;
ID-Token:JWT格式的数据,包含EU身份认证的信息。
UserInfo Endpoint:用户信息接口(受OAuth2保护),当RP使用Access-Token访问时,返回授权用户的信息,此接口必须使用HTTPS。
具体流程
如果是JS应用,其所有的代码都会被加载到浏览器而暴露出来,没有后端可以保证client_secret的安全性,则需要是使用默认模式流程(Implicit Flow)。
如果是传统的客户端应用,后端代码和用户是隔离的,能保证client_secret的不被泄露,就可以使用授权码模式流程(Authentication Flow)。
此外还有混合模式流程(Hybrid Flow),简而言之就是以上二者的融合。
授权码流程
RP发送一个认证请求给OP,其中附带client_id;
OP对EU进行身份认证;
OP返回响应,发送授权码给RP;
RP使用授权码向OP索要ID-Token和Access-Token,RP验证无误后返回给RP;
RP使用Access-Token发送一个请求到UserInfo EndPoint; UserInfo EndPoint返回EU的Claims。
认证请求
RP使用OAuth2的Authorization-Code的方式来完成用户身份认证,所有的Token都是通过OP的Token EndPoint(OAuth2中定义)来发放的。构建一个OIDC的Authentication Request需要提供如下的参数:
- scope:必须。OIDC的请求必须包含值为“openid”的scope的参数。
- response_type:必选。同OAuth2。
- client_id:必选。同OAuth2。
- redirect_uri:必选。同OAuth2。
- state:推荐。同OAuth2。防止CSRF, XSRF。
在OP接收到认证请求之后,需要对请求参数做严格的验证,具体的规则参见http://openid.net/specs/openid-connect-core-1_0.html#AuthRequestValidation,验证通过后引导EU进行身份认证并且同意授权。在这一切都完成后,会重定向到RP指定的回调地址(redirect_uri),并且把code和state参数传递过去。
RP使用上一步获得的code来请求Token EndPoint,这一步桶OAuth2,就不再展开细说了。然后Token EndPoint会返回响应的Token,其中除了OAuth2规定的部分数据外,还会附加一个id_token的字段。id_token字段就是上面提到的ID Token。
ID-Token
上面提到过OIDC对OAuth2最主要的扩展就是提供了ID-Token。下面我们就来看看ID-Token的主要构成:
- iss = Issuer Identifier:必须。提供认证信息者的唯一标识。一般是Url的host+path部分;
- sub = Subject Identifier:必须。iss提供的EU的唯一标识;最长为255个ASCII个字符;
- aud = Audience(s):必须。标识ID-Token的受众。必须包含OAuth2的client_id;
- exp = Expiration time:必须。ID-Token的过期时间;
- iat = Issued At Time:必须。JWT的构建的时间。
- auth_time = AuthenticationTime:EU完成认证的时间。如果RP发送认证请求的时候携带max_age的参数,则此Claim是必须的。
- nonce:RP发送请求的时候提供的随机字符串,用来减缓重放攻击,也可以来关联ID-Token和RP本身的Session信息。
- acr = Authentication Context Class Reference:可选。表示一个认证上下文引用值,可以用来标识认证上下文类。
- amr = Authentication Methods References:可选。表示一组认证方法。
- azp = Authorized party:可选。结合aud使用。只有在被认证的一方和受众(aud)不一致时才使用此值,一般情况下很少使用。
默认流程
默认流程和OAuth中的类似,只不过也是添加了ID-Token的相关内容。
这里需要说明的是:OIDC的说明文档里很明确的说明了用户的相关信息都要使用JWT形式编码。在JWT中,不应该在载荷里面加入任何敏感的数据。如果传输的是用户的User ID。这个值实际上不是什么敏感内容,一般情况下被知道也是安全的。
UserInfo Endpoint
可能有的读者发现了,ID-Token只有sub是和EU相关的,这在一般情况下是不够的,必须还需要EU的用户名,头像等其他的资料,OIDC提供了一组公共的cliams,来提供更多用户的信息,这就是——UserIndo EndPoin。
在RP得到Access Token后可以请求此资源,然后获得一组EU相关的Claims,这些信息可以说是ID-Token的扩展,ID-Token中只需包含EU的唯一标识sub即可(避免ID Token过于庞大和暴露用户敏感信息),然后在通过此接口获取完整的EU的信息。此资源必须部署在TLS之上
关于OAuth的一点思考
User:用户
User Agent:用户代理,如浏览器
Consumer:信息消费者,如Leetcode
Service Provider:分为两个,分别是身份认证提供者(Identity Provider, IDP),如QQ,以及资源提供者(Resource Provider),不过这二者一般是相同的
OAuth并不是为了解决安全或者性能问题。
OAuth也并不会帮我们保存在Consumer的登录态。
OAuth出现的目的就是为了帮我们用一个第三方的账号关联多个应用的账号,它是user赋予consumer在SP的权限,而不是赋予user在consumer的权限。我们的账号一个没少,只不过建立起了一对多的关系,就类似于如果我们想去考驾照,就需要用身份证,授权驾校用我们的身份证信息去公安局证实我们这个人的存在并取回一些其他信息。但是也就到此为止,接下来的事情与OAuth并无关系。
单凭OAuth无法完成在consumer创建账户,IDP不可能也没有责任保存user在consumer的任何信息,它只是在user授权情况下告诉consumer一些信息而已。
为什么一开始就要发送个redirect_uri给IDP?第一次是为了IDP设置返回地302中的Location,可以不验证是不是consumer设置好的,第二次就是验证这个redirect_uri是不是你当初在IDP这里设置的,与第一次过来的是否一样,这一步必须要验证,因为这一步是最关键的,这一步才会返回token。
为什么要绕这么一大圈,为什么要多一步code换access_token?说到底还是信不过浏览器同志,让他当个工具人,让它帮自己地后台去第三方申请一个授权码,然后把这个授权码给自己的后台,再然后自己的后台用这个code去第三方申请token,完事还不告诉浏览器这个token是什么,自己留下了,也就是说浏览器同志从头到尾都没见过token。
Secret有什么用?原因是IDP不信任何人,就信自己给出去的secret。因为redirect_uri是域名,最终到哪里还是要靠IP地址,如果域名是对的,但是域名被攻击者指向了自己的IP,攻击者就会收到token。怎么修改这个DNS指向就涉及DNS污染了,因为DNS会层层缓存,但是又有时间,如果你一直广播告诉路由器或者主机我是leetcode,我是leetcode,时间长了你在这一片局域网中就被认为是leetcode了。但是如果有了secret,就算你带着code过来了IDP,没有我给你的secret,IDP也不会给出token。所以client_id表明自己是谁,只有给了client_secret,IDP才会相信你说的话,并给你token,所以这个secret非常重要,我们的后台又不会相信浏览器同志了,所以我们的浏览器同志从头到尾也没摸过secret。
State又有什么用呢?类似于防御CSRF,保证请求设备的一致性,不过不像CSRF是伪造受害者的请求,而是让受害者登录自己的账号,如果受害者在里面存个比特币账号岂不美哉?具体实现就是攻击者登录之后,正常申请,但是到了IDP返回302以后把请求拦下,不让浏览器发送请求给自己的后台,然后把这个带code的请求链接给受害人,受害人点进去之后就可以拿到access_token成功登录,如果不注意这个账号是不是自己的就上传敏感信息,就很happy。如果有了state,不同的设备我后台都生成一个随机的字符串给前端,攻击者就算把请求发给受害者,他也不知道受害者设备中的state,后台一看你的state和刚开始说好的不一样,就会直接把这个请求丢掉,当然硬要说攻击者把你的state从庞大的互联网的某个请求中给偷到了,那也是绝了,这就属于定点爆破了,就是要搞你,那这个人多半已经混在你的身边了。
为什么最后会返回两个token?因为一个代表你是谁,一个代表你能做什么,你能做的事情随时可以被管理员改变,但是你是谁是固定的,而且一般access_token过期时间都比较短,如果我用着用着就过期了,总不能让用户重新登陆吧,那岂不是回到了起点?
说了半天,这些设计都停留在传输层以上,那我要是搞你的网络层呢?我再散布个ARP病毒呢?搞链路层就有点夸张了。
参考文章:
https://sunra.top/2019/11/16/OAuth%20and%20OIDC/ :OAuth,OIDC简介
https://sunra.top/posts/74ee5df7/ :路由协议
https://sunra.top/posts/dfdf7442/ :ARP原理与防御
https://www.jianshu.com/p/0db71eb445c8 :OAuth认证流程举例
https://www.chrisyue.com/security-issue-about-oauth-2-0-you-should-know.html :OAuth2.0中的安全考虑
https://www.cnblogs.com/linianhui/p/openid-connect-core.html :OIDC文档
https://www.zhihu.com/question/19851243 :OAuth1.0与2.0区别
https://docs.azure.cn/zh-cn/active-directory/azuread-dev/v1-protocols-openid-connect-code :OIDC + AAD
https://www.sciencedirect.com/science/article/pii/S2215098617316750 :云服务中面临的安全问题
https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy :同源策略