OIDC协议
OIDC(OpenID Connect)是在OAuth2上构建了一个身份层,是一个基于OAuth2协议的身份认证标准协议。
OAuth2协议
OAuth2是一个授权协议,它无法提供完善的身份认证功能【1】,OIDC使用OAuth2的授权服务器来为第三方客户端提供用户的身份认证,并把对应的身份认证信息传递给客户端。
使用OAuth2进行认证的常见误区
如果用OAuth2进行认证,会有许多问题。
- 在OAuth2中,
token
被设计成是对客户端
不透明的,但在用户身份认证的上下文环境中,客户端
往往需要能够从token
中派生一些信息。这可以通过定义一个双重目的(dual-purposing)的客户端
以解析和理解的token
来完成。但是OAuth2协议并没有为access token
本身定义特定的格式货结构,因此需要在OAuth2协议的基础上,利用诸如OpenId Connect的ID Token
在响应中提供一个次要的标记,它将和access token
一起发送给客户端
中。一方面,保持了token
是对客户端
的不透明,另一方面,为客户端
提供了其所需的认证信息。 - OAuth保护的是资源,获取用户属性的API(identity API)通常没有办法告诉你用户是否存在。而且,在某些情况下,用户无需身份验证即可获得access token(比如[认证授权] 1.OAuth2授权 - 5.4 Client Credentials Grant)。也就是说
Access Token
并不能和某个用户
关联起来。 - 在某些场合,例如,使用implicit流程(这个流程中直接把acces token作为url的hash参数([认证授权] 1.OAuth2 授权 - 5.2.2 Access Token Response))中,
User Agent
可以获得token
,也就你开辟了一个注入access token
到应用程序外部(并可能在应用程序外部泄露)的地方。如果Client不通过某种机制验证access token,则它无法区分access token是有效的令牌还是攻击的令牌。 - 很可能有一个幼稚的(naive)
客户端
,从其他的客户端
拿到一个有效的token
来作为自己的登录事件。毕竟token
是有效的,对API的访问也会返回有效的用户
信息。问题在于没有用户做任何事情来证明用户
存在,在这种情况下,用户
甚至都没有授权给幼稚的(naive)客户端
。 - 如果攻击者能够拦截或者替换来自Client的一个调用,它可能会改变返回的用户信息,而客户端却无法感知这一情况。这将允许攻击者通过简单地在正确的调用序列中交换用户标识符来模拟一个幼稚的(naive)Client上的用户。
- 基于OAuth 身份(identity)API的最大问题在于,即使使用完全符合OAuth的机制,不同的提供程序不可避免的会使用不同的方式实现身份(identity)API。
OIDC核心概念:ID Token
OAuth2提供了Access Token
来解决授权第三方客户端
访问受保护资源的问题;OIDC在这个基础上提供了ID Token
来解决第三方客户端标识用户身份认证的问题。OIDC的核心在于在OAuth2的授权流程中,一并提供用户的身份认证信息(ID Token
)给到第三方客户端
,ID Token
使用JWT
格式来包装。此外还提供了UserInfo的
接口,用户获取用户的更完整的信息。
参与角色
主要的术语以及概念介绍:
- EU:End User:一个人类
用户
。 - RP:Relying Party ,用来代指OAuth2中的受信任的
客户端
,身份认证和授权信息的消费方; - OP:OpenID Provider,有能力提供EU认证的服务(比如OAuth2中的授权服务),用来为RP提供EU的身份认证信息;
- ID Token:JWT格式的数据,包含EU身份认证的信息。
- UserInfo Endpoint:用户信息接口(受OAuth2保护),当RP使用
Access Token
访问时,返回授权用户的信息,此接口必须使用HTTPS。
工作流程
OIDC的流程由以下5个步骤构成:
-
RP
发送一个认证请求给OP
; -
OP
对EU
进行身份认证,然后提供授权; -
OP
把ID Token
和Access Token
(需要的话)返回给RP
; -
RP
使用Access Token
发送一个请求UserInfo EndPoint
; -
UserInfo EndPoint
返回EU
的Claims
。
注意这里面RP
发往OP
的请求,是属于Authentication
类型的请求,虽然在OIDC中是复用OAuth2的Authorization
请求通道,但是用途是不一样的,且OIDC的AuthN请求中scope参数必须要有一个值为的openid
的参数,用来区分这是一个OIDC的Authentication
请求,而不是OAuth2的Authorization
请求。
什么是ID Token
OIDC对OAuth2最主要的扩展就是提供了ID Token
。ID Token
是一个安全令牌
,是一个授权服务器
提供的包含用户信息(由一组Cliams构成以及其他辅助的Cliams)的JWT
格式的数据结构。
另外ID Token
必须使用JWS进行签名和JWE加密,从而提供认证的完整性、不可否认性以及可选的保密性。一个ID Token
的例子如下:
1 {
2 "iss": "https://server.example.com",
3 "sub": "24400320",
4 "aud": "s6BhdRkqt3",
5 "nonce": "n-0S6_WzA2Mj",
6 "exp": 1311281970,
7 "iat": 1311280970,
8 "auth_time": 1311280969,
9 "acr": "urn:mace:incommon:iap:silver"
10 }
如何获取ID Token
因为OIDC基于OAuth2,所以OIDC的认证流程主要是由OAuth2的几种授权流程延伸而来的,有以下3种:
- Authorization Code Flow:使用OAuth2的授权码来换取
Id Token
和Access Token
。 - Implicit Flow:使用OAuth2的Implicit流程获取
Id Token
和Access Token
。 - Hybrid Flow:混合Authorization Code Flow+Implici Flow。
Resource Owner Password Credentials Grant是需要用途提供账号密码给RP的,账号密码给到RP了,就不再需要ID Token了。
Client Credentials Grant这种方式根本就不需要用户参与,更谈不上用户身份认证了。这也能反映授权和认证的差异,以及只使用OAuth2来做身份认证的事情是远远不够的,也是不合适的。
这里主要介绍基于Authorization Code的认证请求/答复。
基于Authorization Code的认证请求的请求
所有的Token
都是通过Token EndPoint
来发放的。构建一个OIDC的Authentication Request需要提供如下的参数:
- scope:必须。OIDC的请求必须包含值为
openid
的scope的参数。 - response_type:必选。同OAuth2。
- client_id:必选。同OAuth2。
- redirect_uri:必选。同OAuth2。
- state:推荐。同OAuth2。防止CSRF, XSRF。
以上这5个参数是和OAuth2相同的。除此之外,还定义了一些参数【2】例如:
- response_mode:可选。OIDC新定义的参数,用来指定Authorization Endpoint以何种方式返回数据。
一个简单的示例如下:
GET /authorize?
response_type=code
&scope=openid%20profile%20email
&client_id=s6BhdRkqt3
&state=af0ifjsldkj
&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb HTTP/1.1
Host: server.example.com
基于Authorization Code的认证请求的响应
在授权服务器
接收到认证请求之后,需要对请求参数做严格的验证。验证通过后引导EU
进行身份认证并且同意授权。在这一切都完成后,会重定向到RP
指定的回调地址,并且把code
和state
参数传递过去。比如:
HTTP/1.1 302 Found
Location: https://client.example.org/cb?
code=SplxlOBeZQQYbYS6WxSbIA
&state=af0ifjsldkj
获取ID Token
RP
使用上一步获得的code
来请求Token EndPoint
,这一步同OAuth2。然后Token EndPoint
会返回响应的Token
,其中除了OAuth2规定的部分数据外,还会附加一个id_token
的字段。id_token
字段就是上面提到的ID Token
。例如:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
{
"access_token": "SlAV32hkKG",
"token_type": "Bearer",
"refresh_token": "8xLOxBtZp8",
"expires_in": 3600,
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzc
yI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5
NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZ
fV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5Nz
AKfQ.ggW8hZ1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6q
Jp6IcmD3HP99Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJ
NqeGpe-gccMg4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7Tpd
QyHE5lcMiKPXfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoS
K5hoDalrcvRYLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4
XUVrWOLrLl0nx7RkKU8NXNHq-rvKMzqg"
}
其中看起来一堆乱码的部分就是JWT
格式的ID Token
。在RP
拿到这些信息之后,需要对id_token
以及access_token
进行验证。至此,可以说用户身份认证就可以完成了,后续可以根据UserInfo EndPoint
获取更完整的信息。
OIDC协议簇
除了Core核心规范内容多一点之外,另外7个都是很简单且简短的规范,另外Core是基于OAuth2的,也就是说其中很多东西在复用OAuth2。
OpenId Connect定义了一个发现协议,它允许Client轻松的获取有关如何和特定的身份认证提供者进行交互的信息。在另一方面,还定义了一个Client注册协议,允许Client引入新的身份提供程序(identity providers)。通过这两种机制和一个通用的身份API,OpenId Connect可以运行在互联网规模上运行良好,在那里没有任何一方事先知道对方的存在。
UserInfo Endpoint
UserIndo EndPoint
是一个受OAuth2保护的资源。在RP
得到Access Token
后可以请求此资源,然后获得一组EU
相关的Claims
,这些信息可以说是ID Token
的扩展,
GET /userinfo HTTP/1.1
Host: server.example.com
Authorization: Bearer SlAV32hkKG
成功之后相应如下:
HTTP/1.1 200 OK
Content-Type: application/json
{
"sub": "248289761001",
"name": "Jane Doe",
"given_name": "Jane",
"family_name": "Doe",
"preferred_username": "j.doe",
"email": "janedoe@example.com",
"picture": "http://example.com/janedoe/me.jpg"
}
【1】OAuth2中的access_token
【2】[认证 & 授权] 4. OIDC(OpenId Connect)身份认证(核心部分)