OAuth (开放授权) 是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。OAuth是OpenID的一个补充,但是完全不同的服务。
OAuth,一个让人又爱又恨的验证协议,它让许多主流的社交网站(SNS)与网络服务打开了封闭已久的验证大门,它也是在网络上公开个人或私人信息 (private data) 前最主要的验证管道之一,重要的是,在这个协议下,所有公开给外界的私有数据会受到两个阶段的保护,OAuth 保障用户可以在应用程序要求数据前由用户做明确授权,只有授权过的资源才会开放给应用程序读取,而且 OAuth 公开的特性,可以让应用程序在极少量程序代码的修改下,移植到不同的服务继续使用,而 OAuth 协议也让服务端精确的控制要开放的服务,并且提供使用者授权的管道以让使用者能自由控制授权与否,而且客户端应用程序只要利用 HTTP 协议即可使用OAuth 服务。那为什么我会说又爱又恨呢?因为它真的不容易使用,会让想使用它的开发人员头发被抓掉好几百根,最近2周在折腾腾讯开放社区的QQ登陆,QQ登陆也是使用OAuth ,具体参看 【QQ登录】OAuth登录文档,开始的时候使用的是DotnetOpenauth(网址为: http://www.dotnetopenauth.net,可惜被墙了,需要找个梯子),一直进行不下去,问题的原因就是腾讯开放社区的QQ登陆的OAuth不是很标准,无奈之余只好根据QQ登陆文档重头构建一个验证库,我把它托管在codeplex上,地址是http://qqconnect.codeplex.com/,所有的功能还未完成,欢迎有同样需求的各位同学加入。
OAuth 是 Open Authorization 的缩写,由 2006 年起草的新验证标准,当时产业界还有一些不同的协议,像是 Open ID,Shibboleth,WS-Federation 等协定,这些都被称为 Central Authentication Service (中央验证服务),只是这些协议使用上都不怎么容易,有些有明确的软件平台限制,有些则是要装某些套件等,比如微软的WIF,OAuth 则是比较后期出现的服务,但它用起来简单,又兼容于 HTTP 的标准,任何可以产生 HTTP 通讯方式的平台与函式库都可以使用,它也很常搭配 Open ID 使用,所以在一些大型网络服务平台 (ex: Google, Yahoo) 会看到 Open ID + OAuth 的格式,但 OAuth 本身也具备验证能力,不过它是由服务端来做验证。Open ID 亦不是本系列文的重点,所以我们只会在这系列文中看到 OAuth 的使用方式。
那 OAuth 是怎么运作的呢?虽然各大网站都有提供 OAuth 的操作流程,但基本上会是以下图的流程为准:
上图来自 http://oauth.net/core/1.0/#anchor9
大体上会分为三个程序:
1. 客户端应用程序向服务要求一个 Request Token,这个 Token 会用来识别应用程序要求存取的会话。
2. 客户端应用程序开启浏览器 (Desktop Application) 或由服务导向到授权的网页 (Web Application),由使用者决定是否授权,若使用者决定授权时,客户端应用程序会得到一个 Verifier Token,这个 Token 会在稍后向服务要求访问权限。
3. 客户端应用程序向服务提交 Request Token 与 Verifier Token,服务在验证过后核发 Access Token,这个 Token 会在应用程序每次向服务要求资源时,由客户端应用程序提交以验证权限。
那么服务是怎么样决定客户端使用的是 OAuth 协议时,其实很简单,是透过 HTTP Header 中的 Authorization 标头数据,应用程序在 HTTP 要求中加入 OAuth oauth…. 的信息,服务会拆解这个标头中的 OAuth 数据进行检查以验证客户端的权限,例如下面的 HTTP Request 就是典型的 OAuth调用:
POST /accounts/OAuthGetRequestToken HTTP/1.1
Host: www.google.com
Content-Type: application/x-www-form-urlencoded
Authorization: OAuth
oauth_consumer_key="example.com",
oauth_signature_method="RSA-SHA1",
oauth_signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
oauth_timestamp="137131200",
oauth_nonce="4572616e48616d6d65724c61686176",
oauth_version="1.0"
oauth_callback="http://www.example.com/showcalendar.html"
整个 OAuth 最困难的部份就是去搞懂这些在 OAuth 中规定的参数,而这也是使用 OAuth 协议开发客户端应用程序的最大门坎,只要通过这个门坎后,之后的服务访问就变得十分简单。
下面来说明这些参数的意义以及用法。OAuth 使用上最难懂以及测试的,莫过于这些 OAuth 的参数,尤其是在 OWASP 的 Web Security Report (https://www.owasp.org/index.php/Main_Page)之下,又有 Improper Error Handling 的安全漏洞问题,因此在测试 OAuth 时,最容易吃的苦头就是只知道 HTTP 400 (Bad Request) 或 HTTP 401 (Unauthorized),有些服务还会提供一些错误讯息,但也有一点都不提供的,而且就算有提供,也不一定马上就可以意识到问题在哪 (例如: Signature Invalid),往往都要做很多的实验才能真正找到问题在哪,我在测试 QQ登陆 的 OAuth 时就吃了很多的苦头…
- oauth_consumer_key:这是由服务所提供的应用程序密钥,要在每次 HTTP Request 中附加到 Authorization 标头中。
- oauth_consumer_secret:这是由服务提供的应用程序签章密钥,会用来计算 siguature 用。
- oauth_token:由服务在验证后,核发的 Request Token 或 Access Token,要在每次 HTTP Request 中附加到 Authorization 标头中。
- oauth_token_secret:由服务验证后,核发的签章密钥,会和 oauth_consumer_secret 一起用来计算 signature 用。
- oauth_timestamp:由 UTC 时间 1970/1/1 00:00:00 起到当下时间所经过的秒数,但也有一些开发人员使用 64 位的随机数字来代用。
- oauth_nonce:64 位的随机字符串 (64bit random string),这个说法不容易懂,不过可以直接由 oauth_timestamp 转换而得。
- oauth_signature_method:决定 signature 使用的算法,目前 OAuth 1.0 中定义了 HMAC-SHA1,RSA-SHA1 以及 PLAINTEXT 三种格式,但一般会使用 HMAC-SHA1 较多,Google OAuth Playground 默认的算法是 RSA-SHA1。
- oauth_signature:这是最容易出错的一项资料,它会基于三个阶段的 Request 数据,使用 oauth_signature_method 决定的算法,使用 oauth_consumer_key 以及 oauth_token_secret 计算的签章值,但如果 Request 的数据有误时,signature 签章值也会错误,而且不易除错(参数顺序错误也不行…)。
- oauth_version:指定 OAuth 的版本值,目前为 1.0。
- oauth_verifier:在 Verifier 阶段时,由服务核发的检核码,QQ登陆的OAuth的悲剧在这里,这个参数名被改成了oauth_vericode 。
- oauth_callback:由客户端应用程序设定,服务回呼时使用的网址,若应用程序是 Desktop Application 时,此值可设为 oob (Out-Of-Box),此时服务会用不同的方式来进行使用者授权阶段。
这些参数中最需要提的,莫过于 oauth_signature 了,它需要先取得 Request 的参数数据,再用 consumer key 和 token secret 进行哈希计算 (使用 HMAC-SHA1 或是 RSA-SHA1 算法) 后产生的签名值密钥。Request 参数数据被称为基础字符串 (base string),基础字符串是由 HTTP Method, Request URL 以及 Normalized Parameters 组合,它的格式是 {HTTP_METHOD}&{URL}&{PARAMETERS},HTTP Method 可以是 GET 或 POST,URL 会随要求不同而所有不同,而 Normalized Parameters 是一种正规化的参数清单,它要符合两个条件:
1. 必须要是 Lexicographical byte order 的顺序,简单的说,就是要以字母顺序判断排序,如果在参数中的 key 值顺序相同时,就要比对 value 值。
2. 每个参数都要以 key=value 方式组合。
3. 每个值都要经过 UrlEncode() 处理过,这个 UrlEncode() 是一个特别的版本,我们在程序设计时再说明。
例如 Request Token 阶段,必须要有 oauth_consumer_key, oauth_signature_method, oauth_version, oauth_callback, oauth_nonce, oauth_siguature 与 oauth_timestamp 等参数,在处理 Normalized Parameters 时,则一定要使用下列顺序排序:
- oauth_callback=xxx
- oauth_consumer_key=xxx
- oauth_nonce=xxx
- oauth_signature_method=xxx
- oauth_timestamp=xxx
- oauth_version=xxx
而 Normalized Parameters 也规定,不能包含 oauth_signature,所以参数中不可以有 oauth_signature。下列字符串就是 base string 的范例(有分行,真实的情况是一整行):
GET&http%3A%2F%2Fopenapi.qzone.qq.com%2Foauth%2Fqzoneoauth_request_token&
oauth_consumer_key%3D200001%26oauth_nonce%3D1606024431%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1299143758%26oauth_version%3D1.0
在 base string 组合出来后,即可使用 .NET 的 System.Security.Cryptographics 命名空间中的 HMACSHA1 类,使用 ComputeHash() 计算签名值密钥,并设定为 oauth_siguature 参数,作为 OAuth 验证信息的签名用。