uniquejava / blog

My notes regarding the vibrating frontend :boom and the plain old java :rofl.
Creative Commons Zero v1.0 Universal
11 stars 5 forks source link

Thinking in OAuth2 #182

Open uniquejava opened 6 years ago

uniquejava commented 6 years ago

阮一峰: http://www.ruanyifeng.com/blog/2019/04/github-oauth.html OAuth2简明手册: https://aaronparecki.com/oauth-2-simplified 极简: http://tutorials.jenkov.com/oauth2/index.html

我对Authorization Code认证方式的思考

有同事报怨OAuth2中的access code没法重用, 我回头想了一下原因. 仔细推敲才发现OAuth2设计的是如此巧妙.

注: 以下文字中的developer和client均表示使用API的开发者(或APP).

1. 为什么要有authorization code认证方式?

  1. API需要保护, 最起码对调用次数和流量有所要求, 如果给匿名用户调用容易遭到恶意攻击. 所以首先需要client key和client secret(由API Provider给developer一个用户和口令, 用来分辨API是哪个client调用的), 算是对developer的一个认证过程.
  2. API中的敏感数据(比如账户余额)是Customer的, 如果App需要查看, 是需要Customer同意的. 于是customer也有自己的用户名和密码. 但是Customer不可能把自己的密码给developer, 谨慎的Customer也不会在developer开发的app/client上输入用户名密码, 于是有了认证服务器.
  3. 认证服务器必须是Customer信任的, 银行自己的产品比如官网是Customer唯一的选择.
  4. 为了调用到实际的API, 基于以上安全性的考虑, 对Developer的认证和对Customer的认证都必不可少.
  5. 授权码模式: Authorization Code方式就是这样一个两步认证的过程.

2. 为什么需要两步 ?

不能一步得到token吗? 前面说了, 调用API需要提供两组用户名和密码才足够安全.

要是一步就能可以得到token, 那得一下传两个用户名和密码, 传给谁??? Customer只相信银行官网(不会把密码给API Provider) Developer只信任API Provider(不会把密码给银行官网).

所以需要两步: 1) Customer银行页面输入密码后得到access code, 这个access code通过URL传给developer. 2) Developer再通过access code还有自己的密码 交换到一个 access token.

3. 为什么access code不能重用?

首先code是暴露在浏览器URL中的, 非常不安全. 所以可以想像它是一个兑换码, 用一次就作废. 这个code会第一时间通过URL 传给developer, developer通常也是第一时间用它来交换token. 即使其它developer通过窥屏/录像/抓图得到这个code也无法使用.

4. 为什么token可以重用?

  1. Token通过response body并且是https的方式传给developer, 不像code那样容易暴露.
  2. Token发送给了server(比如nodejs), 并不会发给browser, 用户很难获取.
  3. Token完全由Developer和API Provider控制, 一旦泄漏可以随时召回
  4. Token都设置了有效时间, 一段时间即失效.
  5. 得到Token的过程只需一次, developer可以把这个token放在session里, 重复使用. 99%的时间都是使用token调用API.. 只传一个token就可以调用API一是比较方便, 二是token中即没有Developer的密码信息也没有Customer的密码信息, (相对其它的认证方式比如Basic来说更加安全).

其它的方式

  1. 密码模式: 如果Customer完全信任Developer, 愿意把账户密码交给developer, 那么可以使用password的方式由Developer一下传4个参数一步得到token
  2. 客户端模式: 如果API没有什么敏感信息, 可以使用Client Credentials的方式, 也是一次得到Token, 只需要传Developer自己的用户名(Client key)和密码(client secret).
  3. 简化模式: Implicit 和 Access Code方式一样, 只不过是认证服务器通过redirect_url一步返回access_token给client, 这种方式不需要client_secret. 用在纯js客户端(就是client不像我做的是 个nodejs的程序), 不会有session, 然后你想如果client_secret直接从browser传到Authentication Server那么. 用户在浏览器上中就能看到client secret, 不太安全, 所以干脆只做customer authentication不做client authentication, 见: What is the purpose of the implicit grant authorization type in OAuth 2?

References

见理解OAuth 2.0: http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html

一共有4种授权模式, implicit, client_credentials(application), password和authorization code. password是两腿模式(2-legged oauth, 需要server和client参与), 其它都是三腿模式(需要server, client和user三方参与)

和IBM API Connect的整合见我的另一篇总结 #179

Fundamental to the power of OAuth is the notion of delegation. Although OAuth is often called an authorization protocol (and this is the name given to it in the RFC which defines it), it is a delegation protocol. Generally, a subset of a user’s authorization is delegated, but OAuth itself doesn’t carry or convey the authorizations. Instead, it provides a means by which a client can request that a user delegate some of their authority to it. The user can then approve this request, and the client can then act on it with the results of that approval.

In our printing example, the photo-printing service can ask the user, “Do you have any of your photos stored on this storage site? If so, we can totally print that.” The user is then sent to the photo-storage service, which asks, “This printing service is asking to get some of your photos; do you want that to happen?” The user can then decide whether they want that to happen, deciding whether to delegate access to the printing service.

The distinction between a delegation and an authorization protocol is important here because the authorizations being carried by the OAuth token are opaque to most of the system. Only the protected resource needs to know the authorization, and as long as it’s able to find out from the token and its presentation context (either by look- ing at the token directly or by using a service of some type to obtain this information), it can serve the API as required.

https://my.oschina.net/liuyatao19921025/blog/1605948

https://developer.ibm.com/apiconnect/2017/04/11/securing-apis-using-oauth-in-apiconnect/ https://ibm-apiconnect.github.io/pot/lab3_oauth_api.html

https://stackoverflow.com/questions/7522831/what-is-the-purpose-of-the-implicit-grant-authorization-type-in-oauth-2?rq=1

https://github.com/ibm-apiconnect/oidc-blueid

https://communities.ca.com/community/ca-security/ca-single-sign-on/blog/2017/05/30/oauth-openid-connect-and-jwt-what-are-they-and-why-do-you-care-pt2

https://www.ibm.com/support/knowledgecenter/SSFS6T/com.ibm.apic.toolkit.doc/tapim_sec_api_config_scheme_oauth_endpoint.html

uniquejava commented 6 years ago

测试OAuth2

client_credentials伪代码 (java版)

// get token: client_Credential

curl 
https://api.au.apiconnect.ibmcloud.com/hkboc-hackathon/dev/oauth2/token?grant_type=client_credentials&scope=all' 
-X POST 
-H 'Authorization: Basic Base64.encode(user+ ":"+pwd)' 
-H 'Content-Type: application/x-www-form-urlencoded' 

OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder()
  .url("https://api.au.apiconnect.ibmcloud.com/hkboc-hackathon/dev/oauth2/token?grant_type=client_credentials&scope=all")
  .post()
  .addHeader("authorization", "Basic Base64.encode(client_id+ ":"+client_secret)")
  .addHeader("accept", "application/x-www-form-urlencoded")
  .build();

Response response = client.newCall(request).execute();

// call api
curl 'https://api.au.apiconnect.ibmcloud.com/hkboc-hackathon/dev/api/bank-info/atms' 
-H 'Authorization: Bearer token' 
-H 'Accept: application/json' 

OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder()
  .url("https://api.au.apiconnect.ibmcloud.com/hkboc-hackathon/dev/api/bank-info/atms")
  .get()
  .addHeader("authorization", "Bearer token")
  .addHeader("accept", "application/json)
  .build();

Response response = client.newCall(request).execute();
uniquejava commented 5 years ago

上一篇文章介绍了 OAuth 2.0 是一种授权机制,主要用来颁发令牌(token)。本文接着介绍颁发令牌的实务操作。

下面我假定,你已经理解了 OAuth 2.0 的含义和设计思想,否则请先阅读这个系列的上一篇文章。

RFC 6749

OAuth 2.0 的标准是 RFC 6749 文件。该文件先解释了 OAuth 是什么。

OAuth 引入了一个授权层,用来分离两种不同的角色:客户端和资源所有者。......资源所有者同意以后,资源服务器可以向客户端颁发令牌。客户端通过令牌,去请求数据。

这段话的意思就是,OAuth 的核心就是向第三方应用颁发令牌。然后,RFC 6749 接着写道:

(由于互联网有多种场景,)本标准定义了获得令牌的四种授权方式(authorization grant )。

也就是说,OAuth 2.0 规定了四种获得令牌的流程。你可以选择最适合自己的那一种,向第三方应用颁发令牌。下面就是这四种授权方式。

注意,不管哪一种授权方式,第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的。

第一种授权方式:授权码

授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。

这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。

第一步,A 网站提供一个链接,用户点击后就会跳转到 B 网站,授权用户数据给 A 网站使用。下面就是 A 网站跳转 B 网站的一个示意链接。

https://b.com/oauth/authorize?
  response_type=code&
  client_id=CLIENT_ID&
  redirect_uri=CALLBACK_URL&
  scope=read

上面 URL 中,response_type参数表示要求返回授权码(code),client_id参数让 B 知道是谁在请求,redirect_uri参数是 B 接受或拒绝请求后的跳转网址,scope参数表示要求的授权范围(这里是只读)。

image

第二步,用户跳转后,B 网站会要求用户登录,然后询问是否同意给予 A 网站授权。用户表示同意,这时 B 网站就会跳回redirect_uri参数指定的网址。跳转时,会传回一个授权码,就像下面这样。

https://a.com/callback?code=AUTHORIZATION_CODE

上面 URL 中,code参数就是授权码。

image

第三步,A 网站拿到授权码以后,就可以在后端,向 B 网站请求令牌。

https://b.com/oauth/token?
 client_id=CLIENT_ID&
 client_secret=CLIENT_SECRET&
 grant_type=authorization_code&
 code=AUTHORIZATION_CODE&
 redirect_uri=CALLBACK_URL

上面 URL 中,client_id参数和client_secret参数用来让 B 确认 A 的身份(client_secret参数是保密的,因此只能在后端发请求),grant_type参数的值是AUTHORIZATION_CODE,表示采用的授权方式是授权码,code参数是上一步拿到的授权码,redirect_uri参数是令牌颁发后的回调网址。

image

第四步,B 网站收到请求以后,就会颁发令牌。具体做法是向redirect_uri指定的网址,发送一段 JSON 数据。

{    
  "access_token":"ACCESS_TOKEN",
  "token_type":"bearer",
  "expires_in":2592000,
  "refresh_token":"REFRESH_TOKEN",
  "scope":"read",
  "uid":100101,
  "info":{...}
}

上面 JSON 数据中,access_token字段就是令牌,A 网站在后端拿到了。

image

第二种方式:隐藏式

有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)"隐藏式"(implicit)。

第一步,A 网站提供一个链接,要求用户跳转到 B 网站,授权用户数据给 A 网站使用。

https://b.com/oauth/authorize?
  response_type=token&
  client_id=CLIENT_ID&
  redirect_uri=CALLBACK_URL&
  scope=read

上面 URL 中,response_type参数为token,表示要求直接返回令牌。

第二步,用户跳转到 B 网站,登录后同意给予 A 网站授权。这时,B 网站就会跳回redirect_uri参数指定的跳转网址,并且把令牌作为 URL 参数,传给 A 网站。

https://a.com/callback#token=ACCESS_TOKEN

上面 URL 中,token参数就是令牌,A 网站因此直接在前端拿到令牌。

注意,令牌的位置是 URL 锚点(fragment),而不是查询字符串(querystring),这是因为 OAuth 2.0 允许跳转网址是 HTTP 协议,因此存在"中间人攻击"的风险,而浏览器跳转时,锚点不会发到服务器,就减少了泄漏令牌的风险。

image

这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。

第三种方式:密码式

如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为"密码式"(password)。

第一步,A 网站要求用户提供 B 网站的用户名和密码。拿到以后,A 就直接向 B 请求令牌。

https://oauth.b.com/token?
  grant_type=password&
  username=USERNAME&
  password=PASSWORD&
  client_id=CLIENT_ID

上面 URL 中,grant_type参数是授权方式,这里的password表示"密码式",usernamepassword是 B 的用户名和密码。

第二步,B 网站验证身份通过后,直接给出令牌。注意,这时不需要跳转,而是把令牌放在 JSON 数据里面,作为 HTTP 回应,A 因此拿到令牌。

这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用。

第四种方式:凭证式

最后一种方式是凭证式(client credentials),适用于没有前端的命令行应用,即在命令行下请求令牌。

第一步,A 应用在命令行向 B 发出请求。

https://oauth.b.com/token?
  grant_type=client_credentials&
  client_id=CLIENT_ID&
  client_secret=CLIENT_SECRET

上面 URL 中,grant_type参数等于client_credentials表示采用凭证式,client_idclient_secret用来让 B 确认 A 的身份。

第二步,B 网站验证通过以后,直接返回令牌。

这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。

令牌的使用 A 网站拿到令牌以后,就可以向 B 网站的 API 请求数据了。

此时,每个发到 API 的请求,都必须带有令牌。具体做法是在请求的头信息,加上一个Authorization字段,令牌就放在这个字段里面。

curl -H "Authorization: Bearer ACCESS_TOKEN" \
"https://api.b.com"

上面命令中,ACCESS_TOKEN就是拿到的令牌。

更新令牌 令牌的有效期到了,如果让用户重新走一遍上面的流程,再申请一个新的令牌,很可能体验不好,而且也没有必要。OAuth 2.0 允许用户自动更新令牌。

具体方法是,B 网站颁发令牌的时候,一次性颁发两个令牌,一个用于获取数据,另一个用于获取新的令牌(refresh token 字段)。令牌到期前,用户使用 refresh token 发一个请求,去更新令牌。

https://b.com/oauth/token?
  grant_type=refresh_token&
  client_id=CLIENT_ID&
  client_secret=CLIENT_SECRET&
  refresh_token=REFRESH_TOKEN

上面 URL 中,grant_type参数为refresh_token表示要求更新令牌,client_id参数和client_secret参数用于确认身份,refresh_token参数就是用于更新令牌的令牌。

B 网站验证通过以后,就会颁发新的令牌。

写到这里,颁发令牌的四种方式就介绍完了。下一篇文章会编写一个真实的 Demo,演示如何通过 OAuth 2.0 向 GitHub 的 API 申请令牌,然后再用令牌获取数据。