1 场景
第三方应用要来访问我们系统的信息,但该应用不是我们系统的用户。
如通过QQ登录来登录微博的场景,这里微博是第三方应用;
又如在云打印应用上允许应用读取你百度网盘的照片打印,这里云打印应用是第三方应用。
如果第三方应用要访问网盘上你的照片,简单的方法是把用户名密码直接给第三方应用;这种做法的弊端是可访问你所有照片、密码泄露、没法取消授权等,除非改密码,因此要找其他方法,能够限制第三方应用的权限。
Oauth协议要解决的就是 如何让在资源所有者不用把用户名密码告诉第三方应用的情况下第三方应用能访问系统中资源 的问题,术语曰“授权”问题。其引入了授权层来将第三方应用与资源拥有者做区分,即给第三方应用一个有别于资源所有者的角色来访问受保护的资源。具体而言,就是给通过授权层来给第三方应用生成一个token,第三方应用该token作为凭证来访问资源。
OAuth provides a method for clients to access a protected resource on behalf of a resource owner. OAuth addresses these issues by introducing an authorization layer and separating the role of the client from that of the resource owner.
当你的网站实现了OAuth协议后,很多第三方应用就可以来对接你的网站,从而可用你网站的账号去登录第三方应用、或者让第三方应用来访问你网站上的资源。实际上,也有很多基于OAuth协议的协议,比如知名的OpenID协议就是基于OAuth来实现用户身份认证的。OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol.
2 What
OAuth 是一个基于HTTP的授权协议,目前是OAuth 2.0版本。它定义了授权机制,主要用来颁发令牌(token)。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。
令牌的作用相当于密码作用,但有区别:令牌有有效期限制、令牌有效性可被发放方取消、令牌有权限范围(如设置令牌可访问的资源)。令牌与密码一样,校验通过了就可以进入系统,故令牌也要保密而不能泄露,且要设置较短的有效期。
3 几个概念
Authentication:认证信息,通常包括Pricipal和Credential(典型的是用户名和密码)。通过验证此信息来确定访问者的身份,如确定访问者的角色、确定是否是资源的所有者。动词authenticate表示进行认证的行为。
Authorization:授权信息,通常是token。通过此信息来确定是否有访问资源的权限。在工程实践上,很多不大的系统不要求请求者传授权信息,而是直接通过认证结果(如角色)来确定。动词authorize表示进行授权的行为。
Credentials:凭证,如用户名、密码、access token、refresh token、授权码等。Authorization、Authorization分别都是一种Credentials。
Protected Resource:资源,如图片等各种数据。通常存储在服务器上,且是受保护的,访问者得提供 认证信息 或 授权信息 才能访问资源。
User-Agent:代用户执行请求和接收并处理响应者,通常是浏览器。
4 四种角色
Owner、Resource Server、Client、Authorization Server
Resource Owner:资源所有者(controller),可将资源授权给他人访问。当资源所有者指一个人时,称为end-user(用户)。
Resource Server:资源托管者(hosoter),保存资源、保护资源使得不能被随意访问。能够接收和响应 对受保护的资源的 带着access token的请求。通常就是API服务器。
Client:表示一个应用,它带着Resource Owner授予的授权信息请求Resource Server上的资源。业务逻辑中常称为第三方应用(Third-Party application)。
Authorization Server:授权服务器,在对Owner认证后对Client授权。也会对Resource Owner进行认证,若认证通过且获取到了Owner对Resource的授权信息,则会给Client发放一个access token。
注:Resource Server、Authorization Server可以是同一个;一个Authorization Server可以作为多个Resource Server的授权服务器;实际上可以细分下,客户端分为客户端前端、客户端后端。对于客户端,有的授权机制只跟客户端前端、有的只跟客户端后端交互。
上述概念和角色串起来的交互过程:
Resource Owner具有认证信息、Client通过Authorization Server获取授权信息;
对于Protected Resource,Resource Owner通过提供认证信息访问、Client通过提供授权信息访问;
Resource Server通过验证来访者的认证信息或授权信息决定来访者是否可以访问Protected Resource;
上述各个过程彼此间的交互通过User-Agent来完成。
5 OAuth 2.0 授权协议执行流程(Protocol Flow)
总结为三个过程:
1 A、B:Resource Owner给Client授信(authorization grant):可以是前者直接发向后者请求、或者由前者经由Authorization Server作为中介向后者请求授权,协议中推荐后者;返回给Client的信息(grant)是能表示前者授权的可信凭证(crentdential)。OAuth 2.0定义了四种gran以及grant的扩展机制。
An authorization grant is a credential representing the resource owner's authorization (to access its protected resources) used by the client to obtain an access token.
2 C、D:Athorization Server给Client授权:后者对前者发起带有authentication、authorization grant信息的请求,前者对两者进行验证,验证通过则返回access token给后者。
3 E、F:Client带着token向Resource Server请求Protected Resource
前两个过程是关于授权的,标准: https://tools.ietf.org/html/rfc6749;最后一个过程是关于Token的,标准:https://tools.ietf.org/html/rfc6750。其中前两个过程最为重要,我们实现OAuth 2.0协议时主要就是实现者两个过程,可以针对前两个过程实现一个OAuth公共库供各种具体业务服务引用,第三个过程由具体业务服务实现。
5.1 错误响应
不管哪种授权模式,都有可能在交互过程中出错,错误信息格式的定义遵守OAuth 2.0标准,格式如下:
参数 | 类型 | 说明 |
---|---|---|
error |
String | 错误码。在不同模式下可能有不同的值。可能的值详见:https://tools.ietf.org/html/rfc6749#section-4.1.2.1、https://tools.ietf.org/html/rfc6749#section-5.2 |
error_description |
String | 错误信息描述 |
error_uri |
String | 展示错误内容相关的uri。本模块未使用该字段,即该字段值始终为null |
错误响应示例:
{ "error": "invalid_client", "error_description": "unmatched client and redirect uri", "error_uri": null }
不同授权模式(下节介绍)下出异常时的响应信息不尽相同。对于带授权页面的(前两种),redirect uri或client id无效时在授权页面提示Owner而不重定向,否则将错误信息包含在url中进行重定向;对于其他授权模式或刷新token等相关Endpoint,请求有误时也返回上述格式的错误响应。
5.2 四种授信(Authorization Grant)
授权过程中应该考虑的点:token不能出现在url中,等。
对于上述中的B步骤,OAuth 2.0定义了四种Authorization Grant,详情可参阅:https://tools.ietf.org/html/rfc6749#section-4
5.2.1 授权码授权(Authorization Code)
https://tools.ietf.org/html/rfc6749#section-4.1
功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的授权服务进行互动。
过程:client以authorization server为中介向resource owner请求authorization code。
client通过user-agent将resource owner导向authorization server;
authorization server对resource owner进行认证并获取授权信息,然后生成授权码;
authorization server附带上授权码将resource owner导回client;
client带着授权码向authrization server请求token。
包含请求授权码、请求token两个步骤:第三方应用通过浏览器将用户导向授权页面,以让用户选择将哪些资源授权给第三方应用访问、以及选择是否同意授权;当用户做好选择并同意授权后,授权服务生成一个授权码并通过指示浏览器重定向将该授权码返回给第三方应用
1 请求授权码
第三方应用通过浏览器将用户导向授权页面,以让用户选择将哪些资源授权给第三方应用访问、以及选择是否同意授权;当用户做好选择并同意授权后,授权服务生成一个授权码并通过指示浏览器重定向将该授权码返回给第三方应用
请求数据格式:
参数 | 类型 | 是否必须 | 说明 |
---|---|---|---|
response_type |
String | yes | 值恒为code ,表示授权码模式 |
scope |
String list | yes | 第三方应用期望申请的权限列表。权限列表由A定义。 |
client_id |
String | yes | 第三方应用的id,第三方应用在A注册时得到该值 |
redirect_uri |
String | false | 第三方应用接收授权码的uri,未传则采用注册时填的值 |
state |
String | false | 第三方应用自定义的任意状态数据,授权服务在响应请求时会将值原原本本返回 |
第三方在将用户导向本模块的上述Endpoint时将参数附加到Endpoint后面,示例:
http://localhost:8081/oauth/authorize?response_type=code&scope=get_user_info,list_student&redirect_uri=http://www.test.com/api/code&client_id=4af6d97c97744c13&state=abcd
响应数据格式:
用户同意授权后,授权服务将授权码及state附加到第一步所指定的redirect_uri后面作为参数,并返回重定向指令指示浏览器重定向到该目标位置。 返回的重定向location示例:
http://www.test.com/api/code?code=7e9a348c-xxfafdsas&state=abcd
当出错时(如client_id等凭证信息不正确),授权服务会返回错误信息,格式见前一节所述。同样地,这些错误信息的字段会被附加到redirect_uri后面作为参数返回给第三方应用。
2 请求token
第三方应用带着刚获取到的授权码向授权服务请求token,若授权服务校验通过则签发token返回给第第三方应用
请求数据格式:
参数 | 类型 | 是否必须 | 说明 |
---|---|---|---|
grant_type |
String | yes | 值恒为authorization_code ,表示授权码模式 |
code |
String list | yes | 第一步获取的授权码 |
client_id |
String | yes | 第三方应用的id,第三方应用在A注册时得到该值 |
client_secret |
String | yes | 第三方应用的secret,第三方应用在A注册时得到该值 |
redirect_uri |
String | yes | 与第一步中该字段的值一样 |
示例:
{ "grant_type":"authorization_code", "code":"7e9a348c-xxfafdsas", "client_id":"4af6d97c97744c13", "client_secret":"524e62c792be4ea38adc5eaace065afc", "redirect_uri":"http://www.test.com" }
响应数据格式:
参数 | 类型 | 说明 |
---|---|---|
access_token |
String | 授权服务签发的access token |
token_type |
String | token类型,通常为bearer |
expires_in |
String | access token的有效时长,单位为秒 |
scope |
String list | access token所具有的权限 |
refresh_token |
String | 授权服务签发的refresh token |
返回数据示例:
{ "access_token": "Bearer eyJhxx1", "token_type": "bearer", "expires_in": 7200, "scope": [ "get_user_info" ], "refresh_token": "Bearer eyJhxx2" }
5.2.2 隐式授权(Implicit Grant)
https://tools.ietf.org/html/rfc6749#section-4.2
不通过第三方应用程序的服务器,直接在浏览器中向授权服务申请令牌,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌出现在URL中故对访问者是可见的,且客户端不需要认证。
过程:client以authorization server为中介向resource owner请求授权。
client通过user-agent将resource owner导向authorization server。请求时带着redirect uri等信息,该redirect uri通常是个带有js脚本的页面。
authorization server对resource owner进行认证并获取授权信息,然后生成access token;
authorization server将access token加密(因token放在uri中,为了减少暴露风险,进行"加密")后附在client请求时传来的redirect uri上作为uri hash参数;
user agent请求该redirect uri,页面中的js脚本会被执行,js脚本中的逻辑是从uri中提取出access token。
redirect uri对应的页面内容示例:
GET /cb HTTP/1.1 Host: client.example.org HTTP/1.1 200 OK Content-Type: text/html <script type="text/javascript"> // First, parse the query string var params = {}, postBody = location.hash.substring(1), regex = /([^&=]+)=([^&]*)/g, m; while (m = regex.exec(postBody)) { params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]); } // And send the token over to the server var req = new XMLHttpRequest(); // using POST so query isn't logged req.open('POST', 'https://' + window.location.host + '/catch_response', true); req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); req.onreadystatechange = function (e) { if (req.readyState == 4) { if (req.status == 200) { // If the response from the POST is 200 OK, perform a redirect window.location = 'https://' + window.location.host + '/redirect_after_login' } // if the OAuth response is invalid, generate an error message else if (req.status == 400) { alert('There was an error processing the token') } else { alert('Something other than 200 was returned') } } }; req.send(postBody);
与授权码方式类似,但是:
只需要发一次请求;
授权及申请token的过程都在浏览器执行,不需要client后端参与;
不需要client认证:请求时不需要提供client的认证信息(如clientSecret)而是依靠clientId redirectUri进行Client认证、依靠Owner username password进行Owner的认证和授权;
用于请求access token,标准规定不能签发refresh token;
The implicit grant type is used to obtain access tokens (it does not support the issuance of refresh tokens) and is optimized for public clients known to operate a particular redirection URI. These clients are typically implemented in a browser using a scripting language such as JavaScript.
The implicit grant type does not include client authentication, and relies on the presence of the resource owner and the registration of the redirection URI.
The access token may be exposed to the resource owner or other applications with access to the resource owner's user-agent.
Developers should note that some user-agents do not support the inclusion of a fragment component in the HTTP "Location" response header field. Such clients will require using other methods for redirecting the client than a 3xx redirection response -- for example, returning an HTML page that includes a 'continue' button with an action linked to the redirection URI.
此模式与第一种模式很类似,只不过只有一个步骤:
第三方应用通过浏览器将用户导向授权页面,以让用户选择将哪些资源授权给第三方应用访问、以及选择是否同意授权;当用户做好选择并同意授权后,授权服务生成token并通过指示浏览器重定向将该授权码返回给第三方应用。
此模式相当于将第一种模式的第一步和第二步合在一起,但由于会将token放在url中返回,为了降低暴露风险,会对返回url中的参数进行编码,第三方应用需要进行相应解码。此外,此种模式不会返回refresh token。
请求数据格式:
与第一种模式第一个步骤的一样,只不过grant_type
字段的值恒为token
。
响应数据格式:
会将如下字段拼接后进行Base64编码,然后附加到请求时所指定的redirect_uri后面作为hash参数。
参数 | 类型 | 说明 |
---|---|---|
access_token |
String | 授权服务签发的access token |
token_type |
String | token类型,通常为bearer |
expires_in |
String | access token的有效时长,单位为秒 |
scope |
String list | access token所具有的权限 |
state |
String | 第三方应用自定义的任意状态数据,授权服务在响应请求时会将值原原本本返回 |
返回示例:
https://www.test.com/#YWNjZXNzX3Rva2VuPUJlYXJlciBleUpoeHgxJnNjb3BlPWdldF91c2VyX2luZm8mc3RhdGU9YWYmdG9rZW5fdHlwZT1iZWFyZXImZXhwaXJlc19pbj03MjAw
值"access_token=Bearer eyJhxx1&scope=get_user_info&state=af&token_type=bearer&expires_in=7200" 中的参数会以Base64编码,并被附加到请求时指定的redirect_uri后面作为uri hash参数,最终得到上述结果。
第三方应用应进行相应的解码以获取到字段数据。
5.2.3 资源拥有者凭证授权(Resource Owner Password Credentials)
在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而授权服务只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。
此种授权模式只有一个步骤: 资源拥有者(A平台的用户)事先将自己的用户名、密码告诉第三方应用,第三方应用持该信息向A请求资源,此时对A来说,第三方应用相当于A的用户。
请求数据格式:
参数 | 类型 | 是否必须 | 说明 |
---|---|---|---|
grant_type |
String | yes | 值恒为password ,表示resource owner password模式 |
scope |
String list | yes | 第三方应用期望申请的权限列表。权限列表由A定义。 |
client_id |
String | yes | 第三方应用的id,第三方应用在A注册时得到该值 |
username |
String | yes | 资源拥有者的用户名 |
password |
String | yes | 资源拥有者的密码 |
示例:
{ "grant_type":"password", "client_id":"4af6d97c97744c13", "username":"superadmin01", "password":"SenseStudy123", "scope":["all_resource"] }
响应数据格式:
与第一种授权模式中第二个步骤返回的数据格式一样。
5.2.4 客户端凭证授权(Client Credentials)
客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。用于请求access token,标准规定不能签发refresh token。
此种授权模式只有一个步骤: 第三方应用带着事先在A注册的凭证信息向A请求授权,A只要校验凭证有效就向第三方应用签发token。此种授权模式下只要校验凭证有效就发放token,而无需经过资源拥有者同意,故比较危险,只应被用于授予信任的自己公司的应用!最好对应用进行审核。
请求数据格式:
参数 | 类型 | 是否必须 | 说明 |
---|---|---|---|
grant_type |
String | yes | 值恒为clientcredentials ,表示client credentials模式 |
scope |
String list | yes | 第三方应用期望申请的权限列表。权限列表由A定义。 |
client_id |
String | yes | 第三方应用的id,第三方应用在A注册时得到该值 |
client_secret |
String | yes | 第三方应用的secret,第三方应用在A注册时得到该值 |
示例:
{ "grant_type":"password", "client_id":"4af6d97c97744c13", "client_secret":"524e62c792be4ea38adc5eaace065afc", "scope":["all_resource"] }
响应数据格式:
与第一种授权模式中第二个步骤返回的数据格式类似,只不过此种授权模式不会反回refresh token字段。
5.3 Token
5.3.1 Access Token & Refresh Token
Access Token
Access tokens are credentials used to access protected resources. Tokens represent specific scopes and durations of access, granted by the resource owner, and enforced by the resource server and authorization server. The token may denote an identifier used to retrieve the authorization information or ma
Refresh Token
Refresh tokens are credentials used to obtain new access tokens. Refresh tokens are issued to the client by the authorization server and are used to obtain a new access token when the current access token becomes invalid or expires, or to obtain additional access tokens with identical or narrower scope. OAuth 2.0 中Refresh Token是可选的,即并不一定要生成并返回给Client。
5.3.2 刷新access token
当第三方服务申请到的access token到期时,其可以带着refresh token向授权服务申请延长access token的有效期。 由于需要refresh token参数,因此此功能只有第一、三种授权模式下可以使用。
请求数据格式:
参数 | 类型 | 是否必须 | 说明 |
---|---|---|---|
grant_type |
String | yes | 值恒为refresh_token ,表示申请更新token |
scope |
String list | yes | 第三方应用期望申请的权限列表。权限列表由A定义。 |
client_id |
String | yes | 第三方应用的id,第三方应用在A注册时得到该值 |
client_secret |
String | yes | 第三方应用的secret,第三方应用在A注册时得到该值 |
refresh_token |
String | yes | 第三方应用之前申请access token时授权服务同时签发的refresh token |
示例:
{ "grant_type":"password", "client_id":"4af6d97c97744c13", "client_secret":"524e62c792be4ea38adc5eaace065afc", "scope":["all_resource"], "refresh_token":"Bearer eyJhxx2" }
响应数据格式:
与第四种授权模式的返回相同。
5.4 几种授权模式的比较或总结
关于应用注册
不管哪种授权方式,第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的。实际上这就是第三方应用来我们系统注册,就是个注册过程。
关于认证与授权
不管哪种授权模式,Client请求token时候都需要三种credential:Owner认证、Owner授权、Client认证
第一种授权模式:根据Owner输入的username password进行Owner认证和授权(第一步),并生成了授权码,生成的授权码代表了Owner认证和授权的信息;根据请求时带着的clientId clientSecret进行Client认证、根据授权码再次进行Owner认证和授权校验(第二步)。
第二种授权模式:根据Owner输入的username password进行Owner认证和授权;但没传clientSecret所以Client认证如何完成?
第三种授权模式:根据Client请求时带着的Owner的用户名密码进行Owner认证和授权、根据带着的Client的clientId clientSecret进行Client认证。可见,与方式1请求token步骤中的认证和授权方案类似。
第四种授权模式:根据带着的Client的clientId clientSecret进行Client认证、Client就是资源的拥有者故无需进行Owner认证和授权(也认为也是通过clientId clientSecret进行Owner认证和授权)。
关于Endpoint
三种Endpoint:authorization endpoint、token endpoint、redirection endpoint。前两种为授权服务器的、最后者为Client的。
关于是否返回refresh token
各种授权模式下是否签发refresh token:1、3是,2、4否。
关于回调地址重定向的跨域问题
Client需要将授权服务器设为可信的,具体而言,对于前两种方式中的重定向步骤中,Client需要设置以允许来自Authorization Server的跨域请求,否则返回302带有Location Header时会报跨域错误。当然,也可以不按标准规定进行重定向(标准中说明允许如是做,见 https://tools.ietf.org/html/rfc6749#section-1.7),如:可以自定义字段来告知前端代码进行重定向以绕过上述跨域限制。
"While the examples in this specification show the use of the HTTP 302 status code, any other method available via the user-agent to accomplish this redirection is allowed and is considered to be an implementation detail."
带授权页面的授权模式(1、2种)中的几个为什么
state参数的作用
用来防止CSRF:state —— An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent back to the client.
第三方应用请求token时,服务端有两种返回token的方式,可以根据第三方应用是否提供回调地址判断:
1、标准。服务端生成token后调用第三方应用请求token时指定的回调地址,以将token传给第三方应用。第三方应用在该地址handler内实现自己的逻辑,如将token保持在本地、调用资源服务器接口获取资源等。
2、不标准(但Github OAuth采用此):第三方也可不提供回调地址,直接在请求返回后直接拿到了token,然后实现自己的上述后续逻辑。
按理1、2种授权模式中用户授权后授权服务就可以直接把token返回给第三方了,为什么不这样做?
因为授权页面是在授权服务,不可能让授权服务主动去调客户端来给他授权码,故授权成功后授权服务要把信息返回给第三方应用只能通知浏览器通过重定向(总不能让授权服务去调第三方应用的接口)来实现,所以信息只能放在url,而token放url不安全,故加了一个步骤来防止token出现在url:
方式1是先返回授权码然后由第三方应用服务器带着授权码向授权服务请求token,这过程在后台运行,界面上是看不到,但由于包括 访问重定向到第三方服务器的url、第三方服务器在该url hanlder中完成请求token逻辑 两部分,故可能页面会稍微停顿一会。
方式2是将token作为hash参数而不是query参数附在client请求时指定的redirect uri以减少token泄露风险(因为浏览器请求不会将hash发给后端),通常还可先对token做下编码处理以免明文出现在url中。该redirect uri通常是个包含有js script的页面,浏览器访问时会执行js脚本,该脚本逻辑为从redirect uri中提取出token,然后保存在浏览器。
第一步中是否需要client_secret?按理需要,但第三方把授权码传递给授权服务时只能通过url,此时会泄露,所以不需要。此问题与上个问题类似。
为什么不在用户同意授权时就返回资源给第三方?返回不了,当前是在授权服务的前端页面,不可能让授权服务去调第三方接口上传给第三方,用重定向也不行因为不可能把资源放url上(简单的字符串可以,图片呢?)。资源必须是先有客户端主动发起请求才返回的。
为什么不在客户端带着授权码来请求时返回资源而非token?技术上是行得通,但如果这样的话那授权码就相当于是“令牌”了,为了安全,此时:一方面你就得考虑授权码的有效期、刷新等问题;另一方面,即使你考虑好了这些问题,可在前一步中授权码只能通过url返回给客户端,即“令牌”出现在了url,这不安全。那还不如直接返回令牌,可是直接返回令牌的话放在URL又有安全问题,所以只能是标准方案那样了。
其他
OAuth2.0与1.0是不兼容的,共性少。
TLS is mandatory to implement and to use with OAuth 2.0.
This specification leaves a few required components partially or fully undefined (e.g., client registration, authorization server capabilities, endpoint discovery).
6 参考资料
https://tools.ietf.org/html/rfc6749
https://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html
http://www.ruanyifeng.com/blog/2019/04/oauth_design.html
http://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html