几乎所有的 Web 应用程序都提示用户创建账号并登录。为了创建账号,用户被要求提供他们的名字、电子邮件、口令、以及确认口令。不仅这些需要耗费用户很大的负担(50次以上的击键),这还带来安全顾虑,由于用户经常在不同的站点使用相同的口令,而且许多站点并不能有效保护这些凭证。
OpendID 开启了联合标识,这样用户可以使用同样的标识在众多 Web 应用程序之间进行验证,用户和 Web 应用程序都信任标识的提供方,例如 Google、Yahoo 和 Facebook,它们保存用户的配置信息并受应用程序的委托验证用户。这消除了每个 Web 应用程序创建自己的自定义的验证系统的需要,对用户来说,注册和登录遍及互联网的站点也变得更为简易和便捷。
OpenID Connect 是下一代的 OpenID,开发 OpenID Connect 考虑到两个关键因素:
-
向站点传递访问身份验证信息(用户标识)的权限,非常类似与通过委托访问用户的数据(例如用户的日程)。开发人员不应该为这两种不同的用例使用完全不同的协议。特别是许多开发者需要在应用程序中处理这两者。
-
规范应该模块化,在不需要实现自动发现、关联和其他复杂情况的情况,保持规范与上一个版本的 OpenID 的延续性。
基本的 OpenID Connect 流为:
-
通过将用户重定向到标识提供者,应用程序对一个或者多个 OpenID Connect 的作用域 ( openid,profile,email,address ) 请求 OAuth 2.0 授权。
-
在用户被批准 OAuth 授权请求之后,用户的浏览器使用传统的 OAuth 流被重定向回应用程序。应用程序发起对 Check ID 端点的请求。端点与其它场景一样返回用户的标识 ( user_id )。它必须被客户端验证以确认验证有效。
-
如果客户端请求关于用户的额外信息,例如用户的全名、图片和电子邮件地址,客户端可以访问 UserInfo 端点。
由于 OpenID Connect 构建于 OAuth 2.0 之上,且其被设计为模块化的规范,你可以很容易通过兼容的方式实现联合验证。由于这是一本入门手册,本章将初步讨论 OpenID Connect 基本的客户端实现。
ID Token
使用 OpenID Connect 验证,增加了一种额外的类型的 OAuth Token:ID Token。ID Token 或者 id_token 表示被验证用户的标识。它是与 access_token 不同的令牌,其用于获取用户的配置信息,或者在类似授权流中的请求的其它用户数据。
ID Token 是一个 JSON Web Token (JWT)。它被数字签名并且/或者加密由标识提供者表示的用户标识。它可以被视为不透明的字符串, 并传递到 Check Id 端点以进行解释, 而不是使用加密操作来验证身份提供程序。这种灵活性保持了 OAuth 2.0 的精髓,OpenID Connect 比其上一代明显易于使用。
安全属性
尽管最终用户流相当相似, 但由于可能会发生重播攻击, 因此, 身份验证所需的安全措施与授权的安全防范措施有很大的不同。当出于恶意目的多次发送合法凭据时, 会发生重播攻击。
我们希望阻止两种主要类型的重播攻击:
-
攻击者捕获用户登录站点的 OAuth 凭据,随后用于相同的站点。
-
危险的应用程序开发者使用登录他的恶意应用而颁发给某个用户的 OAuth token ,以便在另外的合法应用程序中模拟该用户。
OAuth 2.0 规范要求 OAuth 端点和 API 通过 SSL/TLS 来阻止中间人攻击,比如第一种场景。
防止恶意应用程序开发人员重播他们的应用程序收到的合法 OAuth 凭据, 以便在另一应用程序上模拟他们的用户,需要一个特定于 OpenID Connect 的解决方案。该方案就是 Check ID 端点。Check ID 端点用于验证由 OAuth 提供者颁发的凭据被颁发给正确的应用程序。
建议所有的开发者使用 Check ID 端点,或者解码 JSON Web Token 以验证声称的标识,虽然在应用程序使用服务器端 Web 应用流的时候对有些场景不是必要,UserInfo 端点提供了所有必要的信息。
服务器端的 Web 应用流,当按照规范实现的时候,仅仅通过用户的浏览器颁发授权码。web 应用程序不应从浏览器直接接受访问令牌或标识令牌。访问令牌 ( access token ) 和标识令牌 ( identity token ) 通过服务器对服务器的授权码交换来获取。由于该交换要求已验证的客户端 ID 和授权码所授予的应用的客户端密钥,OAuth token 服务将天然地阻止意外的使用颁发给另一个应用程序的授权码。
另外,客户端的 Web 应用程序流通过浏览器使用哈希片段直接为应用颁发 access token 和标识令牌。访问令牌和标识令牌通常使用 JavaScript 发送给后台的 Web 服务器以验证用户。在这种场景下,Web 服务器必须既加密验证 ID Token,还要调用 Check ID 端点来验证它颁发给正确的应用。这被称为 “验证令牌的听众”。参见 ”Check ID 端点“
获得用户授权
OpenID Connect 获得用户授权的过程几乎与任何支持 OAuth 2.0 的 API获取授权相同。你既可以使用客户端隐式流 ( 如第 3 章 ) ,也可以使用服务器端的 Web 应用流 ( 如第 2 章 )。
使用任何方式的流,客户端生成一个指向 OAuth 授权服务端点的 URL,重定向用户到该 URL 地址,下述参数将被传递:
-
client_id 在你注册应用程序时提供的值。
-
redirect_uri 在授权请求被批准之后,用户将被返回的地址
-
scope
openid
用于基本的 OpenID Connect 请求。如果你的客户端需要访问额外的用户信息,额外的 scope 可以填充到该使用空格分隔的字符串中,例如:profile,email,address。 -
response_type
id_token
表示该应用需要 id_token。另外,token
或者code
必须包含在相应类型中,使用空格分隔这两种响应类型。token
表示表示客户端的 Web 应用程序流,code
表示服务器端的 Web 应用程序流。 -
nonce 在你的实现里,用于防止重放攻击和跨站仿冒攻击的一个唯一值。此值应当是特定请求的一个随机唯一字符串,不可猜测且在客户端保密 ( 可能在服务器的会话中 )。此标识值将包含于 ID token 响应中。
下面是一个完整的授权端点 URL 示例,使用了客户端隐式流:
https://accounts.example.com/oauth2/auth?
scope=openid+email&
nonce=53f2495d7b435ac571&
redirect_uri=https%3A%2F%2Foauth2demo.appspot.com%2Foauthcallback&
response_type=id_token+token&
client_id=753560681145-2ik2j3snsvbs80ijdi8.apps.googleusercontent.com
在用户批准授权请求之后,他们将被重定向回 rediret_uri。由于该请求使用了隐式流,重定向将会包含可用于 UserInfo 端点来获取关于用户信息的 access token。另外,特别对于 OpenID Connect,重定向将包含一个 id_token,它可以发送给 Check ID 端点来获取用户标识。
下面是一个重定向的示例:
https://oauth2demo.appspot.com/oauthcallback#
access_token=ya29.AHES6ZSzX
token_type=Bearer&
expires_in=3600&
id_token=eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiY...
客户端需要从哈希片段中解析出适当的参数,并调用 Check ID 端点来验证响应。
Check ID 端点
Check ID 端点验证伴随 OAuth 2.0 access token 返回的 id_token,以保证其用于正确的客户端,并用于客户端开始验证会话。如上所述,该检查对于客户端的隐式流是必要的 ( 见第 3 章 )。如果该检查不能正确完成,客户端对于重放攻击变得脆弱。
这是一个 Check ID 端点请求的示例:
https://accounts.example.com/oauth2/tokeninfo?
id_token=eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiY...
这是回应
{
"iss" : "https://accounts.example.com",
"user_id" : "113487456102835830811",
"aud" : "753560681145-2ik2j3snsvbs80ijdi8.apps.googleusercontent.com",
"exp" : 1311281970
"nonce" : 53f2495d7b435ac571
}
如果响应没有带有标准的 OAuth 2.0 错误,下面的检查需要执行:
-
验证响应中的
aud
为授权请求中的client_id
值。 -
验证响应中的
nonce
值匹配授权请求中的nonce
值。
如果验证完全成功,user_id
表示在颁发者 ( iss ) 的 scope
中,被验证用户的唯一标识。如果在用户的数据库表中保存该标识值,并且您的应用支持多个标识提供者,建议基于账号保存这两者的值,并被用于后继的验证请求所查询。
UserInfo 端点
Check ID 端点用于为您的应用程序在用户验证中返回唯一标识,许多应用程序需要额外的信息。例如用户的名字,电子邮件地址,头像或者生日等等。这些属性信息可以由 UserInfo 端点返回。
UserInfo 端点是标准的 OAuth 授权 REST API,使用 JSON 表示。如同使用 OAuth 访问任何其它 API 时一样,access_token 既可以作为 Authorization
请求头,也可以作为 URL 查询参数使用。
下面是 UserInfo 端点的请求示例
GET /v1/userinfo HTTP/1.1
Host: accounts.example.com
Authorization: Bearer ya29.AHES6ZSzX
响应内容
{
"user_id": "3191142839810811",
"name": "Example User",
"given_name": "Example",
"family_name": "User",
"email": "user@example.com",
"verified": true,
"profile": "http://profiles.example.com/user",
"picture": "https://photos.profiles.example.com/user/photo.jpg",
"gender": "female",
"birthday": "1982-02-11",
"locale": "en-US"
}
OpenID Connect 没有定义任何特别的 profile 字段作为必须字段,也允许其它的 profile 字段包含在响应中。
性能改进
调用 Check ID 端点的目的是为了验证 id_token 的合法性。但是,这要求额外的对 OpenID Connect 提供者的 HTTP 请求。由于 id_token 作为签名的 JSON Web Token (JWT) 而不是浑浊的大对象返回,额外的请求可以忽略。JWT 包含由 Check ID 端点返回的相同信息,其值被服务器加密签名,可以被客户端所验证。
这给予客户端一个使用 JWT 验证签名的机会 ( 优化性能 ),或者如果客户端希望省略加密而简化对 Check ID 端点的调用。
练习 OpenID Connect
由于 OpenID Connect 规范仍然在开发中,由标识提供者提供的实现体验仍然与规范不同。下面是使用这些体验实现的一些请求和响应
Google 的 OpenID Connect 实现使用下述端点
Google 没有通常的 openid 作用域,但是它的 OpenID Connect 实现支持下面主要的 scope
下面是一个 Google OpenID Connect 实现的授权 URL
https://accounts.google.com/o/oauth2/auth?
scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F
%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile&
state=ABC123456&
redirect_uri=https%3A%2F%2Foauthssodemo.appspot.com%2Foauthcallback&
response_type=token%20id_token&
client_id=8819981768.apps.googleusercontent.com
在该示例中,我们指定 response_type 为 token id_token
,表示我们同时查询 ID 令牌和传统的 OAuth 2.0 access token ( 通过隐式流 )。在用户通过点击 Allow Access
批准请求之后,Google 重定向回到 redirect_uri 地址,并包含 id_token 和 access_token 在 URL 的哈希片段中。id_token 是一个 JSON Web Token (JWT) 且包含用户的 ID。该 ID 令牌可以通过计算加密签名来验证,或者通过 Check ID 端点来验证。为简化起见,我们将展示如何调用 Check ID 端点,这是示例请求:
https://www.googleapis.com/oauth2/v1/tokeninfo?
id_token=eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiY...
这是响应内容
{
"issued_to" : "8819981768.apps.googleusercontent.com",
"user_id" : "113487456102835830811",
"audience" : "8819981768.apps.googleusercontent.com",
"expires_in" : 3465
}
在 Check ID 返回通过确认它为正确的应用程序所颁发为已验证后 ( 通过与应用的 client ID 比较 issued_to 的值 ),应用可能希望获取用户的额外信息。这些信息,例如用户的名字,电子邮件地址等等,可以从 UserInfo 端点作为 JSON 响应获取。OAuth 的 access_token 必须发送以授权请求,这是一个示例。
GET /oauth2/v1/userinfo HTTP/1.1
Host: www.googleapis.com
Authorization: Bearer ya29.AHES6ZSzX
这是响应内容
{
"id": "110634877589748180443",
"email": "ryan.boyd@gmail.com",
"verified_email": true,
"name": "Ryan Boyd",
"given_name": "Ryan",
"family_name": "Boyd",
"link": "http://profiles.google.com/110634877589748180443",
"picture": "https://lh6.googleusercontent.com/-XC1Cwt4OgfY/AAAAAAAAAAI/AAAAAAAACR8/
SU9W99JQFvc/photo.jpg",
"gender": "male",
"birthday": "0000-10-05",
"locale": "en-US"
}
你会注意到,响应中我的生日年份为 0000.我没有这么老,Google 使用该特殊的值表示出生年份没有共享。