OAuth
什么是 OAuth?
高层次来说,OAuth 不是一个 API 或者服务:它是一个授权开放标准,并且任何人都可以实现它。
更明确来说,OAuth 是一个标准,应用程序可以用来提供客户端程序“安全授权访问”。OAuth 在 HTTPs 上工作并通过访问令牌授权设备、API、服务、和应用程序,而不是凭证。
有两个版本的 OAuth:OAuth 1.0a 和 OAuth 2.0 。这些规范是完全不同的,并且不能一起使用,它们之间没有向后兼容。
为什么是 OAuth?
OAuth 的创建,是对直接认证模式的回应。这个模式因 HTTP Basic 认证而闻名,提示用户输入用户名和密码。Basic 认证一直用于服务器端应用程序 API 认证的原始表单:替代每次请求发送用户名和密码,用户发送一个 API key ID 和密钥。在 OAuth 之前,网站提示你键入用户名和密码到一个表单中,它们会以你的身份登录到你的数据中(例如:你的 Gmail 账户)。通常这叫做密码反模式。
为了能创建一个更好的 Web 系统,联合身份被创建用于单点登录(SSO)。在这种场景下,终端用户和他们的身份提供商对话,并且身份提供商生成一个加密的签名令交给应用程认证用户。应用程序信任身份提供商。只要信任关系和签名断言工作,就没问题。下图显示如何工作。
联合身份因 SAML 2.0(一个发布于 2005/03/15 的 OASIS 标准) 出名。它是一个巨大的规范,但是主要的两个组件是它的认证请求协议(也叫做 Web SSO)和它打包身份特性并对其签名的方式,被叫做 SAML 断言。
SAML
SAML 在你的浏览器中是一个基础的会话缓存让你访问 Web 应用程序。它受限于设备文件类型和浏览器之外的场景。
OAuth 和 API
我们构建 API 的方式也发生了很大的变化。在 2015 年,大家投入 WS-* 构建 Web 服务。如今,大多数开发者已经迁移到 REST 和无状态 API。REST 简单的说是 HTTP 命令通过网络推送 JSON 包。
开发者构建大量的 API。如今 API 经济可能是你在会议室中常听见的词汇。公司需要保护他们的 REST API,以一种允许很多设备访问它们的方式。在从前,你要输入你的用户名、密码目录,然后 app 以你的身份登录。这就产生了授权问题。
“我如何允许一个 app 访问我的数据而不需要给它我的密码?”
如果你见过下面的会话,那就是我们在谈论的。一个应用程序请求它是否可以代表你访问数据。
这就是 OAuth。
OAuth 是一个用于 REST/API 委托授权框架。它使应用程序能够在不泄露用户密码的情况下获得用户数据的有限访问(scopes)。它从授权中解绑认证并支持多用户实例定位到不同设备的能力。它支持服务到服务的应用程序、以浏览器为基础的应用程序、移动端/原生 app、控制台程序和电视应用程序。
你可以认为这就像旅馆的房卡,但是用于应用程序。如果你有一个旅馆的房卡,你可以进入你的房间。你如果获取一个旅馆的房卡?你必须在前台做一个认证流程以得到房卡。认证后获得房卡,你可以通过旅馆访问资源。
简单的分解后, OAuth 是:
- App 从用户请求授权
- 用户授权 App 并且传递验证
- App 提交授权验证到服务器获取一个令牌
- 令牌被限制只能访问用户为特定应用授权的内容
OAuth 主要组件
OAuth 构建于下列主要组件之上:
- 作用域和准许(Scopes and Consent)
- 参与者(Actors)
- 客户端(Clients)
- 令牌(Tokens)
- 授权服务(Authorization Server)
- 工作流(Flows)
OAuth 作用域
作用域是当应用程序请求许可时你在授权页面上看到的内容。它们是客户端在请求令牌时请求的权限包(权限包:一堆权限)。它们是程序员在写应用程序时写的。
作用域从强制中解耦授权策略决策。这是 OAuth 的第一个关键点。权限是最重要的。它们不隐藏在需要你逆向工程的应用程序层后面。它们通常在 API 文档中列出:这些作用域是这个应用程序需要的。
你需要获得准许。这叫做第一次使用信任。它在 Web 上是十分重要的用户体验变更。大多数人在 OAuth 之前仅使用用户名和密码会话框。现在你有提出新的页面,你必须培训用户使用它。重新培训网民是困难的。有各种各样的用户,从精通科技的年轻人到不熟悉这一流程的祖父母。它在 Web 上是一个新的概念,如今很重要。现在你需要授权和准许。
准许在应用程序的基础上是可以变化的。它可以是时间敏感的时间范围(天,周,月),但并不是所有平台都允许你选择持续时间。需要注意的是,当你准许时,应用程序可以代表你做事情,例如:LinkedIn 向你的社交网络中的每个人发送垃圾邮件。
OAuth 是互联网规模的解决方案,因为它针对每一个应用程序。通常你有能力登录到一个仪表盘以查看你给什么应用程序准许访问以及取消准许。
OAuth 参与者
OAuth 工作流参与者如下:
- 资源拥有者:在资源服务器上拥有数据。例如:我是我的 Facebook 个人文档的资源拥有者。
- 资源服务器:应用程序要访问的保存数据的 API
- 客户端:要访问你数据的应用程序
- 授权服务器:OAuth 引擎
资源拥有者是一个可以变更不同凭证的角色。它可以是一个终端用户,也可以是一家公司。
客户端可以是公开的和保密的。两者间有一个明显的区别在 OAuth 命名规则中。保密的客户端可以通过保存秘密以被信任。保密的客户端不是在桌面运行或者通过应用商店分发。人们不能逆向工程它们以获取密钥。它们在一个受保护的区域(终端用户不能访问它们)运行。
公共客户端是浏览器、手机 app、和物联网设备。
客户端注册也是 OAuth 的关键组件。它像 OAuth 的车管所。你需要为你的应用程序获取车牌号。这就是你的应用程序如何在授权会话显示 Logo。
OAuth 令牌
访问令牌是客户端用于访问资源服务器(API)的令牌。它们意味着是暂时的。用小时和分钟来考虑它们,而不是天和月。你不需要一个机密的客户端以获取访问令牌。你可以通过公共客户端获取访问令牌。它们被设计为优化互联网规模问题。因为这些令牌是短暂和可向外扩展的,它们不能被取消,你只需要等待它们过期。
另一个令牌是刷新令牌。这有效期是比较长的(天、月、年)。它用于获取新的令牌。要获取刷新令牌,应用程序通常需要具有认证的机密客户端。
刷新令牌可以被取消。当在仪表盘中取消一个应用程序的访问时,你杀掉了它的刷新令牌。这给了你强制客户端轮换秘密的能力。你要做的是,使用你的刷新令牌去获取新的访问令牌,访问令牌通过网络命中访问所有 API 资源。每一次你刷新你的访问令牌你将获取一个新的加密签名令牌。系统中内置了轮换按钮。
OAuth 规范没有定义令牌是什么样子。它可以是任何你想要的格式。但通常是这样,你要这些令牌是一个 JWT(JSON Web Tokens)。简而言之,JWT 是一个安全和值得信任的令牌验证标准。JWT 允许你使用签名对信息(称为声明)进行数字签名,并且可以在以后使用秘密签名密钥进行验证。
令牌从授权服务器上的端点上取回。两个主要的端点是授权端点和令牌端点。它们针对不同的用例进行了分隔。授权端点是你要从用户那获得准许和授权的地方。返回一个用户已经准许的授权许可。然后授权传递到令牌端点。令牌端点处理许可并且说“好极了,这是你的刷新令牌和你的访问令牌”。
你可以使用访问令牌访问 API。一旦令牌过期,你必须带着刷新令牌返回令牌端点以获取新的访问令牌。
缺点是这会引起很多开发者的摩擦。OAuth 最大的痛点是开发者必须管理刷新令牌。你把状态管理推到每个客户端开发者的身上。你得到刷新按钮的好处,但是你制造了开发者的痛点。那就是为什么开发者喜爱 API 密钥的原因。他们只需要拷贝、粘贴密钥,把它们放在一个文本里,然后就完成了。API 密钥对于开发者来说非常方便,但是对于安全性来说非常糟糕。
这里有一个付费游戏的问题。让开发者做 OAuth 流提高安全性,但会产生更多的摩擦。对于工具箱和平台来说是有机会简化事情并帮助管理令牌。幸好,OAuth 现在已经相当成熟了,你最喜欢的语言或框架可能有工具可以简化事情。
我们已经略微聊了关于客户端类型、令牌类型、授权服务器端点和我们如何把它们传递到资源服务器。我提到两个不同的流:获取授权和获取令牌。这些不需要在同一个渠道上产生。前端渠道通过浏览器。浏览器重定向用户到授权服务器,用户给予准许。这在用户的浏览器上产生。一旦用户获得授权许可并交给应用程序,客户端应用程序就不再需要使用浏览器完成 OAuth 流获取令牌了。
令牌将被客户端应用程序使用,以便它能够代表你访问资源。我们叫做后端渠道。后端渠道是一个直接从客户端应用程序发起,到资源服务器的 HTTP 调用,以交换授权许可获得令牌。这些渠道用于不同的流取决于你有什么样的设备能力。
例如,一个前端渠道流通过用户代理授权看起来是这样的:
- 资源拥有人启动流,委托访问,保护资源
- 客户端带着需要的作用域通过浏览器发送授权请求重定向到授权服务器上的授权端点
- 授权服务器返回一个准许会话,说“你是否允许这个应用程序访问这些作用域?”,当然你需要认证应用程序,假如你没有认证你的资源服务器,它会要求你登录。如果你已经缓存了会话缓存,你只会看到准可会话框。查看准可会话,然后同意。
- 授权许可通过浏览器重定向回传到应用程序。这就是前端渠道发生的所有事情。
在这个流程中还有一个变量叫做隐式流程。我们马上就会讲到。
这是在网络中看到的。
Request
GET https://accounts.google.com/o/oauth2/auth?scope=gmail.insert gmail.send
&redirect_uri=https://app.example.com/oauth2/callback
&response_type=code&client_id=812741506391
&state=af0ifjsldkj
这是一个带着一堆查询参数的 GET 请求(不是以 URL 编码为目的的示例)。作用域(Scopes)来自 Gmail 的 API。redirect_uri 是客户端应用程序的 URL,授权许可应该返回到该 URL。这应该匹配来自客户端注册过程的值(在车管所)。你不希望授权退回到外部应用程序。响应类型改变 OAuth 流。客户端 ID 也来自注册流程。状态是一个安全标记,类似 XRSF。
Response
HTTP/1.1 302 Found
Location: https://app.example.com/oauth2/callback?
code=MsCeLvIaQm6bTrgtp7&state=af0ifjsldkj
code
返回的是授权许可,state
确保非伪造并且来自同一个请求。
前端渠道完成后,一个后端渠道流产生了,交换授权码得到访问令牌。
客户端应用程序发送一个访问令牌请求(带着机密客户端证书和客户端 ID)到授权服务器上的令牌端点。这个过程交换一个授权码许可得到一个访问令牌和(可选)一个刷新令牌。客户端通过访问令牌访问受保护的资源。
以下是原始 HTTP 看到的样子。
Request
POST /oauth2/v3/token HTTP/1.1
Host: www.googleapis.com
Content-Type: application/x-www-form-urlencoded
code=MsCeLvIaQm6bTrgtp7&client_id=812741506391&client_secret={client_secret}&redirect_uri=https://app.example.com/oauth2/callback&grant_type=authorization_code
grant_type
是 OAuth 的扩展部分。从预先计算的角度来看,它是一个授权码。它带来了灵活性,可以用不同的方式描述这些授权。这在 OAuth 流中是最常见的类型。
Response
{
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA"
}
响应是 JSON。在使用令牌时,你可以时被动或主动的。主动是在你的客户端中有一个定时器。被动是获取一个错误然后尝试重新获取一个新的令牌。
一旦你有了访问令牌,你可以在认证头(使用 token_type
做为前缀)中使用反问令牌发起对受保护资源的访问。
curl -H "Authorization: Bearer 2YotnFZFEjr1zCsicMWpAA"
https://www.googleapis.com/gmail/v1/users/1444587525/messages
目前你有一个前端渠道,一个后端渠道、不同的端点和不同的客户端。你必须为不同的用例混合和匹配这些。这提高了 OAuth 的复杂性,可能会让人感到困惑。
OAuth 工作流
- 隐式流(Implicit Flow)
- 授权码流(Authorization Code Flow)
- 客户端凭证流(Client Credential Flow)
- 资源拥有者密码流(Resource Owner Password Flow)
- 断言流(Assertion Flow)
- 设备流(Device Flow)
OAuth 不是一个认证协议
总结 OAuth 2.0 的一些错误观念:它不向后兼容 OAuth 1.0。它将所有通信的签名替换为 HTTPS。如今当人们谈论 OAuth 时,他们谈论的是 OAuth 2.0。
因为 OAuth 是一个授权框架不是一个协议,你可能有互操作性问题。在团队实现 OAuth 时有很多变量,你可以需要自定义代码与供应商集成。
OAuth 2.0 不是一个认证框架。
我们一直在讨论委托授权。而不是认证用户,这是关键。OAuth 2.0 单独来说绝对和用户没有一点关系。你只是通过令牌访问资源。
最近几年 OAuth 产生了有巨量的附属。它们又在 OAuth 上添加了复杂性以完成各种企业场景。例如:JWT 可用做互操作令牌,可以对其进行签名和加密。
通过 OAuth 2.0 伪认证
通过 OAuth 登录因 FaceBook Connect 和 Twitter 出名。在这个工作流下,客户端通过访问令牌访问 /me
端点。它只是说客户端可以通过令牌访问资源。人们发明这个假的端点做为通过令牌获取用户信息的一种方式。它不是获取用户信息的标准方式。标准中没有提及要实现这个端点。访问令牌是不透明的。它们为 API 而设计的,而不是为包含用户信息而设计的。
通过身份认证,你要回答的是用户是谁、用户何时认证的和用户如何认证的。通常可以使用 SAML 断言回答这些问题,而不是访问令牌和授权许可。这是为什么我们把这个叫做伪认证的原因。
进入 OpenID Connect
为了解决伪认证问题,OAuth 2.0、Facebook Connect和 SAML 2.0 最好的部分联合创建了 OpenID Connect。OpenID Connect(OIDC)扩展 OAuth 2.0,为客户端提供一个新的签名 id_token
和一个 UserInfo
端点以拉取用户特性。不像 SAML,OIDC 提供一个作用域标准集和身份声明。例如包含:profile
、email
、address
、phone
。
OIDC 是通过使事物完全动态来实现互联网可扩展的。不再需要像 SAML 要求的那样,下载元数据和联合。内置了注册、发现、动态联合的元数据。你可以键入你的邮件地址,然后它动态的发现你的 OIDC 提供商,动态下载元数据,动态知道它要使用什么证书,并且允许 BYOI(Bring Your Own Identity,带上你自己的身份)。它支持企业的高保证级别和关键 SAML 用例。
OIDC 因 Google 和微软出名,两者都是早期使用者。Okta 在 OIDC 中也做了巨大投入。
初始请求中所改变是,它包含标准作用域(例如:openid
和 email
):
Request
GET https://accounts.google.com/o/oauth2/auth?
scope=openid email&
redirect_uri=https://app.example.com/oauth2/callback&
response_type=code&
client_id=812741506391&
state=af0ifjsldkj
Response
HTTP/1.1 302 Found
Location: https://app.example.com/oauth2/callback?
code=MsCeLvIaQm6bTrgtp7&state=af0ifjsldkj
code
返回的是授权许可,state
确保不被伪造并且来自同一个请求。
并且授权许可响应令牌包含一个 ID Token。
Request
POST /oauth2/v3/token HTTP/1.1
Host: www.googleapis.com
Content-Type: application/x-www-form-urlencoded
code=MsCeLvIaQm6bTrgtp7&client_id=812741506391&
client_secret={client_secret}&
redirect_uri=https://app.example.com/oauth2/callback&
grant_type=authorization_code
Response
{
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ..."
}
看得出这是在 OAuth 上很好的分层,返回一个 ID Token 做为结构化令牌。ID Token 是一个 JWT。JWT 比一个巨大的以 XML 为基础的 SAML 断言要小的多,并且可以极具效率的在不同的设备间传递。一个 JWT 包含三个部分:消息头、消息体和签名。消息头表明用什么算法对其进行签名,Claims(声明) 在消息体中,签名内容保存在签名中。
OIDC 工作流包含以下几个步骤:
- 发现 OIDC 元数据
- 执行 OAuth 工作流获取 ID Token 和访问令牌
- 获取 JWT 密钥然后(可选)动态注册客户端程序
- 本地验证 JWT ID Token,以内置日期和签名为基础
- 根据需要通过访问令牌获取用户其它特性
OAuth 2.0 总结
OAuth 2.0 是一个用于授权访问 API 的授权框架。它包含资源拥有者授权或者给予准许客户端请求作用域。授权许可用于交换访问令牌和刷新令牌(取决于工作流)。有多个工作流适用不同的客户端和授权场景。 JWT 可以用于结构化令牌,在授权服务器和资源服务器之间。
OAuth 有一个很大的安全的表面。确保使用安全的工具箱验证所有输入。
OAuth 不是一个认证协议。OIDC 扩展 OAuth 2.0 以适应验证场景。
OAuth 2.0
OAuth 2.0 是工业级标准授权协议。 OAuth 2.0 聚焦于客户端开发者便利性,为网页应用程序、桌面客户端、手机、客厅设备提供特定的授权流程。
RFC6749 OAuth 2.0 授权框架
RFC6750 OAuth 2.0 授权框架:使用 Bearer Token
OAutho 2.0 安全当前最佳实践——注意,此文档仅是草稿,很有可能随时会被替换或者被弃用。在这里引用的目的是为了提升对 OAuth 2.0 的理解和当前应用场景下 OAuth 2.0 存在的一些问题。
PKCE
RFC7636:Proof Key for Code Exchange
PKCE 是授权码流(Authorization Code Flow)的一个扩展,以阻止几个攻击并且能够安全的在公共客户端间执行 OAuth 交换。
起初它设计用于保护移动端 app,由于它能够阻止授权码注入,所以对于任何 OAuth 客户端来说它都很有用,甚至使用客户端密钥的 Web 应用程序。
权限类型
Authorization Code
授权码权限类型用于保密,并且公共客户端通过交换授权码以获取访问令牌。
用户通过重定向 URL 返回客户端后,应用程序将会从 URL 中获取授权码并用于获取一个访问令牌。
在这个流下,推荐所有客户端都使用 PKCE 扩展以便提供更好的安全性。
OAuth 2.1
OAuth 2.1 是一项正在努力中的工作。以巩固和简化 OAuth 2.0 中最常用的功能。
自从 2012 年发布初版 OAuth 2.0 (RFC6749)以来,几个新的征求意见稿(RFC)也发布了,要从核心规则中添加或移除功能。包括云原生 Apps OAuth 2.0(RFC8252)、PKCE、浏览器应用授权、OAutho 2.0 安全当前最佳实践。
和 OAuth 2.0 主要的不同如下:
- 所有使用授权码流程的客户端都需要 PKCE(Proof Key for Code Exchange)
- 必须使用精确字符串匹配来比较重定向 URI
- 从 OAuth 2.0 中删除隐式授权,也叫简单模式(response_type=token)
- 从 OAuth 2.0 中移除密码模式
- Bearer 令牌使用,移除在 URI 中 query 字符串中使用 Bearer 令牌
- 公共客户端刷新令牌必须受发送端限制或是一次性的
基于 OAuth 2.0 构建的协议
OpenID Connect
什么是 OpenID Connect?
OpenID Connect 1.0 是在 OAuth 2.0 协议上的身份层。它允许客户端基于认证服务器执行认证以验证终端用户的身份,同时以一个可互操作和类 REST 的方式获取终端用户的基础信息。
OpenID Connect 允许所有的客户端类型(包含以 Web 为基础的、移动端、JavaScript 客户端)请求和接收关于认证会话和终端用户的信息。规范套件是可扩展的,允许参与者使用可选功能(比如:身份信息加密、OpenID 提供者发现、会话管理)。当可选功能对参与者有意义时。
OpenID Connect 如何不同于 OpenID 2.0?
OpenID Connect 如同 OpenID 2.0 一样执行很多相同的任务,但以 API 友好的方式,并且用于原生和移动端应用程序。OpenID Connect 定义可选机制以实现健壮的签名和加密。鉴于集成 OAuth 1.0a 和 OpenID 2.0 需要扩展,在 OpenID Connect 中,协议自己集成了 OAuth 2.0 的能力。
规范组织
OpenID Connect 1.0 规范由以下文档组成:
- Core —— 定义 OpenID Connect 核心功能:在 OAuth 2.0 上构建认证并且使用 Claims 和终端用户沟通信息
- Discovery ——(可选)定义客户端如何动态发现 OpenID 提供者信息
- Dynamic Registration ——(可选)定义客户端如何动态注册 OpenID 提供者
- OAuth 2.0 Mutiple Response Types —— 定义几个具体的 OAuth 2.0 返回类型
- OAuth 2.0 Form Post Response Mode —— (可选)定义如何返回 OAuth 2.0 授权响应参数(包含 OpenID Connect 认证响应参数)使用 HTML 表单值,通过用户代理使用 HTTP POST 自动提交
- RP-Initiated Logout ——(可选)定义一个依赖方如何要求 OpenID 提供商从终端用户退出登录
- Session Management ——(可选)定义如何管理 OpenID Connect 会话,包括以 postMessage 为基础的退出登陆和 RP-initiated 退出登录功能
- Front-Channel Logout ——(可选)定义一个前端渠道退出登录机制(在 RP 页面上不使用 OP iframe)
- Back-Channel Logout ——(可选)定义一个退出登录机制,直接使用 back-channel 在 OP 和 RP 沟通以退出登录
- OpenID Connect Federation ——(可选)定义 OP 和 RP 集合如何通过使用联合操作建立信任
注: OP = OpenID Provider,OpenID 提供商。RP = Relying Party,依赖方。
两个实现指南参考,适用于基于以 Web 为基础的依赖方实现的独立服务: