协议背景
在我们之前的文章深入理解Spring Cloud Security OAuth2及JWT已经提到,OAuth2 本身设计是一个资源访问的授权协议而不是一个身份认证协议,虽然也可以通过提供一个用户身份的资源接口来实现SSO,但终归是野路子。同时缺乏标准化的用户身份验证规范,不同服务商返回的用户信息格式不统一,从而导致跨服务商的身份验证复杂化。
那有啥办法可以既用到OAuth2认证的灵活性,又可以在 token 中携带用户信息呢?OIDC就是在这个背景之上设计出来的一个专门用于身份认证的规范(可以类比于SAML2),OIDC全称为OpenID Connect,由OpenID Foundation这个非营利性组织制定(其实背后还是微软、谷歌、salesforce这些大佬)。它是OAuth2的一个超集,基于OAuth2认证规范(RFC 6749 和 6750),同时扩展了与认证相关的信息,可以作为标准的SSO服务器使用。
较OAuth2,OIDC中的概念更加接近于SAML2:
- EU:End User,用户。
- RP:Relying Party ,用来代指OAuth2中的受信任的客户端,身份认证和授权信息的消费方;
- OP:OpenID Provider,有能力提供EU身份认证的服务方(比如OAuth2中的授权服务),用来为RP提供EU的身份认证信息;
- ID-Token:JWT格式的数据,包含EU身份认证的信息。
- UserInfo Endpoint:用户信息接口(受OAuth2保护),当RP使用ID-Token访问时,返回授权用户的信息,此接口必须使用HTTPS。
完整术语参见http://openid.net/specs/openid-connect-core-1_0.html#Terminology
OIDC的认证流程:
- RP发送认证请求到OP
- OP对EU进行认证(AuthN)及授权(AuthZ)
- OP在认证请求响应报文中返回access_token及id_token
- RP携带Access Token发送请求到UserInfo Endpoint
- UserInfo Endpoint返回EU的完整Claims
OIDC协议
OIDC本身是有多个规范构成,其中包含一个核心的规范,多个可选支持的规范来提供扩展支持,简单的来看一下:
- Core:必选。定义OIDC的核心功能,在OAuth 2.0之上构建身份认证,以及如何使用Claims来传递用户的信息。
- Discovery:可选。发现服务,使客户端可以动态的获取OIDC服务相关的元数据描述信息(比如支持那些规范,接口地址是什么等等)。
- Dynamic Registration :可选。动态注册服务,使客户端可以动态的注册到OIDC的OP(这个缩写后面会解释)。
- OAuth 2.0 Multiple Response Types :可选。针对OAuth2的扩展,提供几个新的response_type。
- OAuth 2.0 Form Post Response Mode:可选。针对OAuth2的扩展,OAuth2回传信息给客户端是通过URL的querystring和fragment这两种方式,这个扩展标准提供了一基于form表单的形式把数据post给客户端的机制。
- Session Management :可选。Session管理,用于规范OIDC服务如何管理Session信息。
- Front-Channel Logout:可选。基于前端的注销机制,使得RP(这个缩写后面会解释)可以不使用OP的iframe来退出。
- Back-Channel Logout:可选。基于后端的注销机制,定义了RP和OP直接如何通信来完成注销。
下图是协议族的全景图,一般我们用core这个核心规范就可以了
ID Token
OIDC对OAuth2进行的主要扩展(用户身份验证)就是在access_token这一步的返回中增加了ID Token,为JWT格式。其中包含授权服务器对用户验证的Claims和其它请求的Claims。
在ID Token中,以下Claims适用于使用OIDC的所有OAuth2:
- iss,必须,发行机构Issuer,大小写敏感的URL,不能包含query参数
- sub,必须,用户身份Subject,Issuer为End-User分配的唯一标识符,大小写敏感不超过255 ASCII自符
- aud,必须,特别的身份Audience,必须包含OAuth2的client_id,大小写敏感的字符串/数组
- exp,必须,iat到期时间Expire,参数要求当前时间在该时间之前,通常可以时钟偏差几分钟,unix时间戳
- iat,必须,JWT颁发时间Issuer at time,unix时间戳
- auth_time,End-User验证时间,unix时间戳。当发出max_age或auth_time Claims时,必须。
- nonce,用于将Client session和ID Token关联,减轻重放攻击,大小写敏感字符串
- acr,可选,Authentication Context Class Reference
- amr,可选,Authentication Methods References,JSON字符串数组,身份验证的表示符,如可能使用了密码和OTP身份验证方式
- azp,可选,Authorized party,被授权方。如果存在必须包含OAuth2的Client ID,仅当ID Token有单个Audience且与授权方不同时,才需要此Claim
ID Token可能包含其它Claims,任何未知的Claims都必须忽略。ID Token必须使用JWS进行签名,并分别使用JWS和JWE进行可选的签名和加密,从而提供身份验证、完整性、不可抵赖性和可选的机密性。如果对ID Token进行了加密,则必须先对其签名,结果是一个嵌套的JWT。ID Token不能使用nonce作为alg值,除非所使用的响应类型没有从Authorization Endpoint返回任何ID Token(如Authorization Code Flow),并且客户端在注册时显示请求使用nonce。
认证流程演示
auth0 提供了一个 OpenID Connect Playground,非常直观的演示了 OIDC认证的全过程,下面使用这个Playground来进行认证步骤的演示(机密信息都已经做了混淆,仅供演示):
- 系统未登陆状态,302重定向到 auth0 的 OIDC认证服务器
https://samples.auth0.com/authorize?client_id=kbyuFDidLLm280LIw
&redirect_uri=https://openidconnect.net/callback
&scope=openid profile email phone address
&response_type=code
&state=bb88011dbf3707434d64553a1ab4f2337a5c9fad
这时候在auth0这里进行用户认证(也可以使用谷歌、facebook等用户体系认证,这里以谷歌为例)
- 认证通过后,应用拿到了授权码(code)后,从后台发起请求获取access_token 和 id_token
POST https://samples.auth0.com/oauth/token
grant_type=authorization_code
&client_id=kbyuFDidLLm280LIw
&client_secret=60Op4HFM0I8ajz0WdiStA
&redirect_uri=https://openidconnect.net/callback
&code=PbS1Rs5-R9PR21QYiaQg_23792iHAQnf
返回信息:
HTTP/1.1 200
Content-Type: application/json
{
"access_token": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwiaXNzIjoiaHR0cHM6Ly9zYW1wbGV...",
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRJRCI6ImtieXVGRGlkTExt...",
"scope": "openid profile email phone address",
"expires_in": 86400,
"token_type": "Bearer"
}
可以看到,第1步和第2步和标准的OAuth2认证是一模一样的,唯一不同的是第2步多返回了一个id_token字段,这个字段的jwt的payload解包后就是携带的用户信息。
- 验证id_token的jwt签名
验证通过后的解包数据:
{
"clientID": "kbyuFDidLLm280LIw",
"created_at": "2024-09-11T12:06:05.079Z",
"email": "foobar@gmail.com",
"email_verified": true,
"family_name": "foo",
"given_name": "bar",
"identities": [
{
"provider": "google-oauth2",
"user_id": "10202",
"connection": "google-oauth2",
"isSocial": true
}
],
"name": "foo bar",
"nickname": "foobar",
"picture": "https://lh3.googleusercontent.com/a/AC",
"updated_at": "2024-09-11T14:41:09.966Z",
"user_id": "google-oauth2|10202202",
"user_metadata": {},
"app_metadata": {},
"iss": "https://samples.auth0.com/",
"sub": "google-oauth2|1020220",
"aud": "kbyuFDidLLm2",
"iat": 1726065714,
"exp": 1726101714
}
如果认证框架用的 spring security,则此时会自动将数据填入构造好的OidcUser对象中,就可以直接使用了。