什么是 OIDC ?
OIDC 是 OpenID Connect 的简称,OIDC=(Identity, Authentication) + OAuth 2.0。它在 OAuth2 上构建了一个身份层,是一个基于 OAuth2 协议的身份认证标准协议。OAuth2 是一个授权协议,它无法提供完善的身份认证功能,OIDC 使用 OAuth2 的授权服务器来为第三方客户端提供用户的身份认证,并把对应的身份认证信息传递给客户端,且可以适用于各种类型的客户端(比如服务端应用,移动 APP,JS 应用),且完全兼容 OAuth2,也就是说搭建一个 OIDC 的服务后,也可以当作一个 OAuth2 的服务来用。
OIDC 应用场景:
OIDC 已经有很多的企业在使用,比如 Google 的账号认证授权体系,Microsoft 的账号体系也部署了 OIDC,当然这些企业有的也是 OIDC 背后的推动者。除了这些之外,有很多各个语言版本的开源服务端组件,客户端组件等等(http://openid.net/developers/certified/)。
理解 OIDC 的前提是需要理解 OAuth2,这里假设大家都有 OAuth2 的基础。
OIDC 主要术语
- 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 协议族
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 直接如何通信来完成注销。
上图是官方给出的一个 OIDC 组成结构图,我们暂时只关注 Core 的部分。就像当初的 AJAX 一样,它其实并不是一个新的技术,而是结合很多已有的技术,按照规范的方式组合起来。同理,OIDC 也不是新技术,它主要是借鉴 OpenId 的身份标识,OAuth2 的授权和 JWT 包装数据的方式,把这些技术融合在一起就是 OIDC。
OIDC 核心概念
OAuth2 提供了 Access Token 来解决授权第三方客户端访问受保护资源的问题;OIDC 在这个基础上提供了 ID Token 来解决第三方客户端标识用户身份认证的问题。OIDC 的核心在于在OAuth2 的授权流程中,一并提供用户的身份认证信息(ID Token)给到第三方客户端,ID Token 使用 JWT 格式来包装,得益于 JWT(JSON Web Token)的自包含性,紧凑性以及防篡改机制,使得 ID Token 可以安全的传递给第三方客户端程序并且容易被验证。此外还提供了 UserInfo 的接口,用户获取用户的更完整的信息。
OIDC 主要术语
主要术语见第一章节,完整术语参考:https://openid.net/specs/openid-connect-core-1_0.html#Terminology。
OIDC 工作流程
从抽象的角度来看,OIDC的流程由以下5个步骤构成:
- RP 发送一个认证请求给 OP;
- OP 对 EU 进行身份认证,然后提供授权;
- OP 把 ID Token 和 Access Token(需要的话)返回给 RP;
- RP 使用 Access Token 发送一个请求 UserInfo EndPoint;
- UserInfo EndPoint 返回 EU 的 Claims。
上图取自 Core 规范文档,其中 AuthN=Authentication,表示认证;AuthZ=Authorization,代表授权。注意这里面 RP 发往 OP 的请求,是属于 Authentication 类型的请求,虽然在 OIDC 中是复用 OAuth2 的 Authorization 请求通道,但是用途是不一样的,且 OIDC 的 AuthN 请求中 scope
参数必须要有一个值为的 openid
的参数(后面会详细介绍 AuthN 请求所需的参数),用来区分这是一个 OIDC 的 Authentication 请求,而不是 OAuth2 的 Authorization 请求。
ID Token
上面提到过 OIDC 对 OAuth2 最主要的扩展就是提供了 ID Token。ID Token 是一个安全令牌,是一个授权服务器提供的包含用户信息(由一组 Cliams 构成以及其他辅助的 Cliams)的 JWT 格式的数据结构。ID Token 的主要构成部分如下(使用 OAuth2 流程的 OIDC):
-
iss
= Issuer Identifier:必须。提供认证信息者的唯一标识。一般是一个 https 的 url(不包含 querystring 和 fragment 部分)。 -
sub
= Subject Identifier:必须。iss
提供的 EU 的标识,在iss
范围内唯一。它会被 RP 用来标识唯一的用户。最长为255个 ASCII 个字符。 -
aud
= Audience(s):必须。标识 ID Token 的受众。必须包含 OAuth2 的 client_id。 -
exp
= Expiration time:必须。过期时间,超过此时间的 ID Token 会作废不再被验证通过。 -
iat
= Issued At Time:必须。JWT 的构建的时间。 -
auth_time
= AuthenticationTime:EU 完成认证的时间。如果 RP 发送 AuthN 请求的时候携带max_age
的参数,则此 Claim 是必须的。 -
nonce
:RP 发送请求的时候提供的随机字符串,用来减缓重放攻击,也可以来关联 ID Token 和 RP 本身的 Session 信息。 -
acr
= Authentication Context Class Reference:可选。表示一个认证上下文引用值,可以用来标识认证上下文类。 -
amr
= Authentication Methods References:可选。表示一组认证方法。 -
azp
= Authorized party:可选。结合 aud 使用。只有在被认证的一方和受众(aud
)不一致时才使用此值,一般情况下很少使用。
ID Token 通常情况下还会包含其他的 Claims(毕竟上述 Claim 中只有 sub
是和 EU 相关的,这在一般情况下是不够的,必须还需要 EU 的用户名,头像等其他的资料,OIDC 提供了一组公共的 Cliams,请参考 http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims 。另外 ID Token 必须使用 JWS 进行签名或 JWE 加密,从而提供认证的完整性、不可否认性以及可选的保密性。一个 ID Token 的例子如下:
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJGZUdHREhwdmE2TXRGeWZ3Z0tTQlJ6VGFvYzcwSER4b25DWjk3MmY3QzB3In0.eyJqdGkiOiJmNjIxM2JkMi0zNGU0LTQzM2UtYjdjYy01YzljNGYwMWM4N2YiLCJleHAiOjE2MjcyODE4NzMsIm5iZiI6MCwiaWF0IjoxNjI3MjgxNTczLCJpc3MiOiJodHRwOi8vMTkyLjE2OC43LjIwOjkwODAvYXV0aC9yZWFsbXMvY2NtYWRhcHQiLCJhdWQiOiJ3ZWJfYXBwIiwic3ViIjoiNGM5NzM4OTYtNTc2MS00MWZjLTgyMTctMDdjNWQxM2EwMDRiIiwidHlwIjoiSUQiLCJhenAiOiJ3ZWJfYXBwIiwibm9uY2UiOiI1X1lUbkgtc1ktM1lrVTFqRndqa2oxT2w3VmtadkNuR1BNRWx5bGNqNWlvIiwiYXV0aF90aW1lIjoxNjI3MjgxNTczLCJzZXNzaW9uX3N0YXRlIjoiZWRkZDk5OGItMjk1YS00ZmZlLTgyMWUtZGVjYmQyMWZmMTU1IiwiYWNyIjoiMSIsInVwbiI6ImFkbWluIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImFkZHJlc3MiOnt9LCJuYW1lIjoiQWRtaW4gQWRtaW5pc3RyYXRvciIsImdyb3VwcyI6WyJST0xFX0VVUkVLQV9VU0VSIiwiUk9MRV9FTkRQT0lOVF9VU0VSIiwiUk9MRV9DT05GSUdfQURNSU4iLCJvZmZsaW5lX2FjY2VzcyIsIlJPTEVfQ09ORklHX1VTRVIiLCJST0xFX0FETUlOIiwidW1hX2F1dGhvcml6YXRpb24iLCJST0xFX0VORFBPSU5UX0FETUlOIl0sInByZWZlcnJlZF91c2VybmFtZSI6ImFkbWluIiwiZ2l2ZW5fbmFtZSI6IkFkbWluIiwiZmFtaWx5X25hbWUiOiJBZG1pbmlzdHJhdG9yIiwiZW1haWwiOiJhZG1pbkBsb2NhbGhvc3QuY29tIn0.P9PEDpOue8t-wve4OEa2QM9lPmum_yksv-c9o1n7sLiaZqBMGEai8LFeGwBOwnObVYHaGPlZw2c3lXIaasS9bddqUk2ZeLu28WX4XMy7Xk_07zLVKYs21_HMJ8WE9EGToKVBDlPFD9H72vrstf0NAz27ZxjiiK0kWiNS5Dd4jRRWO4H837fXgK-rWoMITEUBEGU1iMYWH7uc6NkaraVXR5x5QP-rwItLtmlS2bgLFqVn6b5ztXxNiv4uzZ0gslzAEHMiJ078ehrKHWd2Qwlov2CBGQR4-RWdLcHIjbcu_SF7Z6Je6iQd9hKOsk4SoEfLUYKz8DdikqYk-Igbsp-DwA
{
"alg": "RS256",
"typ": "JWT",
"kid": "FeGGDHpva6MtFyfwgKSBRzTaoc70HDxonCZ972f7C0w"
}
.
{
"jti": "f6213bd2-34e4-433e-b7cc-5c9c4f01c87f",
"exp": 1627281873,
"nbf": 0,
"iat": 1627281573,
"iss": "http://authserver.com/auth/realms/ccm",
"aud": "web_app",
"sub": "4c973896-5761-41fc-8217-07c5d13a004b",
"typ": "ID",
"azp": "web_app",
"nonce": "5_YTnH-sY-3YkU1jFwjkj1Ol7VkZvCnGPMElylcj5io",
"auth_time": 1627281573,
"session_state": "eddd998b-295a-4ffe-821e-decbd21ff155",
"acr": "1",
"upn": "admin",
"email_verified": true,
"address": {},
"name": "Admin Administrator",
"groups": [
"ROLE_EUREKA_USER",
"ROLE_ENDPOINT_USER",
"ROLE_CONFIG_ADMIN",
"offline_access",
"ROLE_CONFIG_USER",
"ROLE_ADMIN",
"uma_authorization",
"ROLE_ENDPOINT_ADMIN"
],
"preferred_username": "admin",
"given_name": "Admin",
"family_name": "Administrator",
"email": "admin@localhost.com"
}
.
P9PEDpOue8t-wve4OEa2QM9lPmum_yksv-c9o1n7sLiaZqBMGEai8LFeGwBOwnObVYHaGPlZw2c3lXIaasS9bddqUk2ZeLu28WX4XMy7Xk_07zLVKYs21_HMJ8WE9EGToKVBDlPFD9H72vrstf0NAz27ZxjiiK0kWiNS5Dd4jRRWO4H837fXgK-rWoMITEUBEGU1iMYWH7uc6NkaraVXR5x5QP-rwItLtmlS2bgLFqVn6b5ztXxNiv4uzZ0gslzAEHMiJ078ehrKHWd2Qwlov2CBGQR4-RWdLcHIjbcu_SF7Z6Je6iQd9hKOsk4SoEfLUYKz8DdikqYk-Igbsp-DwA
认证
解释完了 ID Token 是什么,下面就看一下 OIDC 如何获取到 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 + Implicit Flow。
OAuth2 中还有基于 Resource Owner Password Credentials Grant 和 Client Credentials Grant 的方式来获取 Access Token,为什么OIDC没有扩展这些方式呢?
Resource Owner Password Credentials Grant 是需要用户提供账号密码给 RP 的。
Client Credentials Grant 这种方式根本就不需要用户参与,更谈不上用户身份认证了。这也能反映授权和认证的差异,以及只使用 OAuth2 来做身份认证的事情是远远不够的,也是不合适的。
基于 Authorization Code 的认证请求
这种方式使用 OAuth2 的 Authorization Code 的方式来完成用户身份认证,所有的 Token 都是通过 Token EndPoint(OAuth2 中定义:https://tools.ietf.org/html/rfc6749#section-3.2 )来发放的。构建一个 OIDC 的 Authentication Request 需要提供如下的参数:
-
scope
:必须。OIDC 的请求必须包含值为“openid”的scope
的参数。 -
response_type
:必选。同 OAuth2。 -
client_id
:必选。同 OAuth2。 -
redirect_uri
:必选。同 OAuth2。 -
state
:推荐。同 OAuth2。防止 CSRF,XSRF。
以上这5个参数是和 OAuth2 相同的。除此之外,还定义了如下的参数:
-
response_mode
:可选。OIDC 新定义的参数(OAuth 2.0 Form Post Response Mode),用来指定 Authorization Endpoint 以何种方式返回数据。 -
nonce
:可选。ID Token 中的出现的nonce
就是来源于此。 -
display
: 可选。指示授权服务器呈现怎样的界面给 EU。有效值有(page
,popup
,touch
,wap
),其中默认是page
。page
是普通的页面,popup
是弹出框,touch
是支持触控的页面,wap
是移动端页面。 -
prompt
:可选。这个参数允许传递多个值,使用空格分隔。用来指示授权服务器是否引导 EU 重新认证和同意授权(consent
,就是 EU 完成身份认证后的确认同意授权的页面)。有效值有(none
,login
,consent
,select_account
)。none
是不实现现任何认证和确认同意授权的页面,如果没有认证授权过,则返回错误login_required
或interaction_required
。login
是重新引导 EU 进行身份认证,即使已经登录。consen
t 是重新引导 EU 确认同意授权。select_account
是假如 EU 在授权服务器有多个账号的话,允许 EU 选择一个账号进行认证。 -
max_age
:可选。代表 EU 认证信息的有效时间,对应 ID Token 中auth_time
的 claim。比如设定是20分钟,则超过了时间,则需要引导 EU 重新认证。 -
ui_locales
:可选。用户界面的本地化语言设置项。 -
id_token_hint
:可选。之前发放的 ID Token,如果 ID Token 经过验证且是有效的,则需要返回一个正常的响应;如果有误,则返回对应的错误提示。 -
login_hint
:可选。向授权服务器提示登录标识符,EU 可能会使用它登录(如果需要的话)。比如指定使用用户使用 admin 账号登录,当然 EU 也可以使用其他账号登录,这只是类似 html 中 input 元素的 placeholder。 -
acr_values
:可选。Authentication Context Class Reference values,对应 ID Token 中的acr
。此参数允许多个值出现,使用空格分割。
以上是基于 Authorization Code 方式的 OIDC 的认证请求所需的参数。在 OIDC 的其他认证流程中也会有其他的参数或不同的参数值(稍有差异),一个简单的示例如下:
http://authserver.com/auth/realms/ccm/protocol/openid-connect/auth?response_type=code&client_id=web_app&scope=openid%20address%20ccm%20email%20microprofile-jwt%20offline_access%20phone%20profile%20roles%20web-origins&state=9OCCDDURKeOiNkZ7f3Q3TB02nDseNxw_Y-KCmqbmrWE%3D&redirect_uri=http://mywebsite.com/login/oauth2/code/oidc&nonce=uVZRRguqZXZxyZ2wQLrhVvJ8oxnpGvgQGPth1uH3yTQ
response_type: code
client_id: web_app
scope: openid address ccm email microprofile-jwt offline_access phone profile roles web-origins
state: 9OCCDDURKeOiNkZ7f3Q3TB02nDseNxw_Y-KCmqbmrWE=
redirect_uri: http://mywebsite.com/login/oauth2/code/oidc
nonce: uVZRRguqZXZxyZ2wQLrhVvJ8oxnpGvgQGPth1uH3yTQ
以上 URL 可以是一个普通的GET
请求,但通常是基于302
的重定向GET
请求。
基于 Authorization Code 的认证请求的响应
在授权服务器接收到认证请求之后,需要对请求参数做严格的验证,具体的规则参见 http://openid.net/specs/openid-connect-core-1_0.html#AuthRequestValidation ,验证通过后引导 EU 进行身份认证并且同意授权。在这一切都完成后,会重定向到 RP 指定的回调地址,并且把 code
和 state
参数传递过去。比如:
Location: http://mywebsite.com/login/oauth2/code/oidc?state=9OCCDDURKeOiNkZ7f3Q3TB02nDseNxw_Y-KCmqbmrWE%3D&session_state=c110ddc8-c6c1-4a95-bd9e-cd8d84b4dd70&code=7138b4b3-8c2b-4016-ad98-01c4938750c6.c110ddc8-c6c1-4a95-bd9e-cd8d84b4dd70.1eabef67-6473-4ba8-b07c-14bdbae4aaed
state: 9OCCDDURKeOiNkZ7f3Q3TB02nDseNxw_Y-KCmqbmrWE=
session_state: c110ddc8-c6c1-4a95-bd9e-cd8d84b4dd70
code: 7138b4b3-8c2b-4016-ad98-01c4938750c6.c110ddc8-c6c1-4a95-bd9e-cd8d84b4dd70.1eabef67-6473-4ba8-b07c-14bdbae4aaed
获取 ID Token
RP 使用上一步获得的 code
来请求 Token EndPoint,这一步同 OAuth2。然后 Token EndPoint 会返回响应的 Token,其中除了 OAuth2 规定的部分数据外,还会附加一个 id_token
的字段。id_token
字段就是上面提到的 ID Token。
请求示例:
POST /auth/realms/ccm/protocol/openid-connect/token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic d2ViX2FwcDp3ZWJfYXBw
grant_type=authorization_code&code=7138b4b3-8c2b-4016-ad98-01c4938750c6.c110ddc8-c6c1-4a95-bd9e-cd8d84b4dd70.1eabef67-6473-4ba8-b07c-14bdbae4aaed&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
响应示例:
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"
}
在 RP 拿到这些信息之后,需要对 id_token
以及 access_token
进行验证。至此,用户身份认证就基本完成,后续可以根据 UserInfo EndPoint 获取更完整的信息。
Implicit Flow 和 Hybrid Flow
与 Authorization Code Flow 相比,Implicit Flow 在 EU 完成认证授权动作的后即返回 Token,而不需要 RP 专门发起获取 Token 的请求。
UserInfo Endpoint
UserInfo EndPoint 是一个受 OAuth2 保护的资源。在 RP 得到 Access Token 后可以请求此资源,然后获得一组 EU 相关的 Claims,这些信息是 ID Token 的扩展,通过此接口获取完整的 EU 的信息。
请求示例:
GET /auth/realms/ccm/protocol/openid-connect/userinfo HTTP/1.1
Host: server.example.com
Authorization: Basic d2ViX2FwcDp3ZWJfYXBw
响应示例:
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"
}
其中 sub
代表 EU 的唯一标识,这个 Claim 是必须的,其他的都是可选的。
OIDC Discovery 规范
Discovery 定义了一个服务发现的规范,它定义了一个 API(/.well-known/openid-configuration),这个 API 返回一个 json 数据结构,其中包含了一些 OIDC 中提供的服务以及其支持情况的描述信息,这样可以使得 OIDC 服务的 RP 可以不再硬编码 OIDC 服务接口信息。这个 API 返回的示例信息如下,更完整的信息在官方的规范中有详细的描述和解释说明:http://openid.net/specs/openid-connect-discovery-1_0.html 。
{
"authorization_endpoint": "http://authserver.com/auth/realms/ccm/protocol/openid-connect/auth",
"check_session_iframe": "http://authserver.com/auth/realms/ccm/protocol/openid-connect/login-status-iframe.html",
"claim_types_supported": [
"normal"
],
"claims_parameter_supported": false,
"claims_supported": [
"aud",
"sub",
"iss",
"auth_time",
"name",
"given_name",
"family_name",
"preferred_username",
"email"
],
"code_challenge_methods_supported": [
"plain",
"S256"
],
"end_session_endpoint": "http://authserver.com/auth/realms/ccm/protocol/openid-connect/logout",
"grant_types_supported": [
"authorization_code",
"implicit",
"refresh_token",
"password",
"client_credentials"
],
"id_token_encryption_alg_values_supported": [
"RSA-OAEP",
"RSA1_5"
],
"id_token_encryption_enc_values_supported": [
"A128GCM",
"A128CBC-HS256"
],
"id_token_signing_alg_values_supported": [
"PS384",
"ES384",
"RS384",
"HS256",
"HS512",
"ES256",
"RS256",
"HS384",
"ES512",
"PS256",
"PS512",
"RS512"
],
"introspection_endpoint": "http://authserver.com/auth/realms/ccm/protocol/openid-connect/token/introspect",
"issuer": "http://authserver.com/auth/realms/ccm",
"jwks_uri": "http://authserver.com/auth/realms/ccm/protocol/openid-connect/certs",
"registration_endpoint": "http://authserver.com/auth/realms/ccm/clients-registrations/openid-connect",
"request_object_signing_alg_values_supported": [
"PS384",
"ES384",
"RS384",
"ES256",
"RS256",
"ES512",
"PS256",
"PS512",
"RS512",
"none"
],
"request_parameter_supported": true,
"request_uri_parameter_supported": true,
"response_modes_supported": [
"query",
"fragment",
"form_post"
],
"response_types_supported": [
"code",
"none",
"id_token",
"token",
"id_token token",
"code id_token",
"code token",
"code id_token token"
],
"scopes_supported": [
"openid",
"address",
"ccm",
"email",
"microprofile-jwt",
"offline_access",
"phone",
"profile",
"roles",
"web-origins"
],
"subject_types_supported": [
"public",
"pairwise"
],
"tls_client_certificate_bound_access_tokens": true,
"token_endpoint": "http://authserver.com/auth/realms/ccm/protocol/openid-connect/token",
"token_endpoint_auth_methods_supported": [
"private_key_jwt",
"client_secret_basic",
"client_secret_post",
"client_secret_jwt"
],
"token_endpoint_auth_signing_alg_values_supported": [
"RS256"
],
"token_introspection_endpoint": "http://authserver.com/auth/realms/ccm/protocol/openid-connect/token/introspect",
"userinfo_endpoint": "http://authserver.com/auth/realms/ccm/protocol/openid-connect/userinfo",
"userinfo_signing_alg_values_supported": [
"PS384",
"ES384",
"RS384",
"HS256",
"HS512",
"ES256",
"RS256",
"HS384",
"ES512",
"PS256",
"PS512",
"RS512",
"none"
]
}
它包含有授权的 URL,获取 token 的 URL,注销 token 的 URL,以及其对 OIDC 的扩展功能支持的情况等等信息。
OAuth 2.0 扩展
Multiple Response Types
前面解释授权请求的时候讲到,请求参数中有一个 response_type
的参数,其允许的值有 code
和 token
两个,在这两个的基础上,OIDC 增加了一个新值 id_token
,详细信息定义在 http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html 。
- code:OAuth 2.0 定义的,用于获取
authorization_code
。 - token:OAuth 2.0 定义的,用于获取
access_token
。 - id_token:OIDC 定义的,用于获取
id_token
。
OIDC 是支持三种类型的 response_type
的,不但如此,OIDC 还允许了可以组合这三种类型,即在一个 response_type
中包含多个值(空格分隔)。比如当参数是这样的时候 response_type=id_token token
,OIDC 服务就会把 access_token 和 id_token 一并给到调用方。OIDC 对这些类型的支持情况体现在上面提到的 Discovery 服务中返回的response_types_supported
字段中:
"response_types_supported": [
"code",
"none",
"id_token",
"token",
"id_token token",
"code id_token",
"code token",
"code id_token token"
]
Form Post Response Mode
在 OAuth 2.0 的授权码流程中,当 response_type
设置为 code
的时候,OAuth 2.0 的授权服务会把 authorization_code
通过 URL 的 query 部分传递给调用方,比如:
http://mywebsite.com/login/oauth2/code/oidc?state=9OCCDDURKeOiNkZ7f3Q3TB02nDseNxw_Y-KCmqbmrWE%3D&session_state=c110ddc8-c6c1-4a95-bd9e-cd8d84b4dd70&code=7138b4b3-8c2b-4016-ad98-01c4938750c6.c110ddc8-c6c1-4a95-bd9e-cd8d84b4dd70.1eabef67-6473-4ba8-b07c-14bdbae4aaed
在 OAuth 2.0 的隐式授权流程中,当 response_type
设置为 token
的时候,OAuth 2.0 的授权服务会直接把 access_token
通过 URL 的 fragment 部分传递给调用方,比如:
http://mywebsite.com/oauth2/token/oidc#access_token=2YotnFZFEjr1zCsicMWpAA&state=xyz&expires_in=3600
OAuth 2.0 中,上面的两种情况是其默认行为,并没有参数来显示的控制。OIDC 在保持 OAuth 2.0 的默认行为的基础上,增加了一个名为 response_mode
的参数,并且增加了一种通过 form 表单传递信息的方式,即 form_post
,详细信息定义在 http://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html 。OIDC 服务对这个扩展的支持情况体现在上面提到的 Discovery 服务中返回的 response_modes_supported
字段中:
"response_modes_supported": [
"query",
"fragment",
"form_post"
]
当reponse_mode设置为form_post的时候,OIDC则会返回如下的信息:
<html>
<head><title>Submit This Form</title></head>
<body onload="javascript:document.forms[0].submit()">
<form method="post" action="http://mywebsite.com/oauth2/token/oidc">
<input type="hidden" name="state"
value="DcP7csa3hMlvybERqcieLHrRzKBra"/>
<input type="hidden" name="id_token"
value="eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJzdWIiOiJqb2huIiw
iYXVkIjoiZmZzMiIsImp0aSI6ImhwQUI3RDBNbEo0c2YzVFR2cllxUkIiLC
Jpc3MiOiJodHRwczpcL1wvbG9jYWxob3N0OjkwMzEiLCJpYXQiOjEzNjM5M
DMxMTMsImV4cCI6MTM2MzkwMzcxMywibm9uY2UiOiIyVDFBZ2FlUlRHVE1B
SnllRE1OOUlKYmdpVUciLCJhY3IiOiJ1cm46b2FzaXM6bmFtZXM6dGM6U0F
NTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZCIsImF1dGhfdGltZSI6MTM2Mz
kwMDg5NH0.c9emvFayy-YJnO0kxUNQqeAoYu7sjlyulRSNrru1ySZs2qwqq
wwq-Qk7LFd3iGYeUWrfjZkmyXeKKs_OtZ2tI2QQqJpcfrpAuiNuEHII-_fk
IufbGNT_rfHUcY3tGGKxcvZO9uvgKgX9Vs1v04UaCOUfxRjSVlumE6fWGcq
XVEKhtPadj1elk3r4zkoNt9vjUQt9NGdm1OvaZ2ONprCErBbXf1eJb4NW_h
nrQ5IKXuNsQ1g9ccT5DMtZSwgDFwsHMDWMPFGax5Lw6ogjwJ4AQDrhzNCFc
0uVAwBBb772-86HpAkGWAKOK-wTC6ErRTcESRdNRe0iKb47XRXaoz5acA"/>
</form>
</body>
</html>
OIDC 会话管理
如何主动的撤销认证呢(也就是我们常说的退出登录)?OIDC 单独定义了3个独立的规范来完成这件事情:
- Session Management:可选,Session 管理,用于规范 OIDC 服务如何管理 Session 信息。
- Front-Channel Logout:可选,基于前端的注销机制。
- Back-Channel Logout:可选,基于后端的注销机制。
其中 Session Management 是 OIDC 服务自身管理会话的机制,Back-Channel Logout 则是定义在纯后端服务之间的一种注销机制,应用场景不多。这里重点关注一下 Front-Channel Logout 这个规范,它的使用最为广泛,其工作的具体的流程如下(结合 Session Management 规范):
在上图中的2和3属于 session management 这个规范的一部分。其中第2步中,OIDC 的退出登录的地址是通过 Discovery 服务中返回的 end_session_endpoint
字段提供的 RP 的。其中还有一个 check_session_iframe
字段则是供纯前端的 JS 应用来检查 OIDC 的登录状态用的。
"check_session_iframe": "http://authserver.com/auth/realms/ccm/protocol/openid-connect/login-status-iframe.html",
"end_session_endpoint": "http://authserver.com/auth/realms/ccm/protocol/openid-connect/logout",
4、5、6、7这四步则是属于 Front-Channel Logout 规范的一部分。
这一部分中重点有两个信息:
- RP 退出登录的 URL 地址(这个在 RP 注册的时候会提供给 OIDC 服务);
- URL 中的
sessionid
参数,这个参数一般是会包含在 ID Token 中给到 OIDC 客户端,或者在认证完成的时候以一个独立的sessionid
的参数给到 OIDC 客户端,通常来讲都是会直接把它包含在 ID Token 中以防止被篡改。
实践与案例
单点登录 SSO
一个用户 G 要登录网站 A,A 有三个子站,域名分别是 a1.com、a2.com、a3.com。如果 A 想要为用户提供更流畅的登录体验,让用户 G 登录了 a1.com 之后也能顺利登录其他两个域名,就可以创建一个身份认证服务,来支持 a1.com、a2.com 和 a3.com 的登录。这就是我们说的单点登录(SSO),“一次登录,畅通所有”。那么,可以使用 OIDC 协议标准来实现这样的单点登录吗?
通常来讲,SSO 包括统一的登录和统一的登出两部分。基于 OIDC 实现的 SSO 主要是利用 OIDC 服务作为用户认证中心统一入口,使得所有的需要登录的地方都交给 OIDC 服务来做。也就是把需要进行用户认证的客户端中的“用户认证”这部分都剥离出来交给 OIDC 认证中心来做。
CCM 微服务架构的 OAuth2.0 + OIDC 实践
CCM 微服务架构提供的是非开放服务,API 接口直接供配套的 UI 调用,此时,网关充当的角色是 OAuth 2.0 Login 和 OAuth 2.0 Client,微服务应用的角色是 OAuth 2.0 Resource Server。整体框架如下:
认证授权服务 Keycloak
CCM 采用的认证授权服务是 Keycloak,Keycloak 是由 Red Hat 开源的,适用于现代应用程序和服务的开源身份和访问管理解决方案。可以很方便的为应用程序和安全服务添加身份验证,无需处理存储用户或对用户进行身份验证,开箱即用。包含用户联合,身份代理和社交登录等功能。
官网介绍的特点有:
- Single-Sign On:Login once to multiple applications
- Standard Protocols:OpenID Connect, OAuth 2.0 and SAML 2.0
- Centralized Management:For admins and users
- Adapters:Secure applications and services easily
- LDAP and Active Directory:Connect to existing user directories
- Social Login:Easily enable social login
- Identity Brokering:OpenID Connect or SAML 2.0 IdPs
- High Performance:Lightweight, fast and scalable
- Clustering:For scalability and availability
- Themes:Customize look and feel
- Extensible:Customize through code
- Password Policies:Customize password policies
简单总结一下,包括以下特点:
- 是一个独立的认证授权服务器,支持 SSO,提供完整的认证授权解决方案
- 主要基于 OpenID Connect,OAuth 2.0 和 SAML 2.0 协议
- 基本的登录、注册,以及登录注册页面主题自定义
- 很人性化的用户界面管理,比如用户、角色、session、Clients 等等的管理
- 具有独立的数据库,用于存储用户等认证授权数据
- 支持联合数据存储,比如集成 LDAP 服务器;提供 SPI 扩展,比如 user Storage SPI,可以让用户的一部分数据存储在业务的数据库,一部分存储在 keycloak 的数据库
- 轻量级、快速,可扩展性强,支持分布式部署
- 提供多种语言库集成 keycloak
- 提供管理 API,用于管理 keycloak 中所有的认证授权对象
- 支持不同的密码策略
- Docker-compose 一键安装,同时 Windows 解压版解压后即可使用
SAML:安全断言标记语言( Security Assertion Markup Language,缩写:SAML )是一个基于 XML 的开源标准数据格式,它在当事方之间交换身份验证和授权数据,尤其是在身份提供者和服务提供者之间交换。
LDAP:轻型目录访问协议( Lightweight Directory Access Protocol,缩写:LDAP)是一个开放的,中立的,工业标准的应用协议,通过 IP 协议提供访问控制和维护分布式信息的目录信息。
Spring Security:OAuth 2.0 Login / OAuth 2.0 Client
OAuth 2.0 Login 特性提供了让用户可以使用他们在 OAuth 2.0 服务(如 GitHub、Keycloak)或 OpenID Connect 1.0 服务(如谷歌)的现有帐户登录到应用程序的能力。
OAuth 2.0 Client 特性提供了对 OAuth 2.0 授权框架中定义的客户端角色的支持。
Spring Boot 2.x 示例
以下是网关关于 OAuth 2.0 的一些关键配置。
配置认证服务器
spring:
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: http://localhost:9080/auth/realms/ccmadapt
registration:
oidc:
client-name: 'Keycloak'
client-id: web_app
client-secret: web_app
其中,spring.security.oauth2.client.provider
和 spring.security.oauth2.client.registration
配置的是一个 Key-Value 集合,代表一到多个认证或 OAuth2.0 服务提供者和对应 Client 配置,Key 是当前客户端为该服务提供者分配的唯一标识符,与下文的 registrationId
对应。
设置重定向 URI
重定向 URI 是应用程序中的路径,EU 在用户代理(浏览器)进行身份验证并授予对 OAuth Client 的访问权后将被重定向回该路径。
默认的重定向 URI 模板是
{baseUrl}/login/oauth2/code/{registrationId}
。registrationId
是 ClientRegistration 的唯一标识符,在此示例中值是 keycloak。无特殊处理不需要修改该 URI 模板。
如果 OAuth Client 运行在代理服务器后面,检查代理服务器配置,确保通过X-Forward-*
传递浏览器访问的 IP 或者域名、端口等信息给OAuth Client ,以确保 OAuth Client 正确生成{baseUrl}
。
ServerHttpSecurity 配置
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
// @formatter:off
http.oauth2Login()
.and()
.oauth2ResourceServer()
.jwt();
http.oauth2Client();
// @formatter:on
return http.build();
}
可以看到,网关中同时开启了 OAuth 2.0 Client 和 OAuth 2.0 Resource Server 特性,网关的 OAuth 2.0 Resource Server 不是必须的,这儿开启是因为 CCM Dashboard 需要调用网关的 Spring Boot Actuator 接口获取应用数据,此时, CCM Dashboard 是 OAuth 2.0 Client,网关是 OAuth 2.0 Resource Server,如果没有此功能需求,亦或者 Spring Boot Actuator 端口与应用端口隔离,不需要安全认证,可关闭网关的 OAuth 2.0 Resource Server 特性。
Spring Security:OAuth 2.0 Resource Server
Spring Security 支持使用两种形式的 OAuth 2.0 Bearer Tokens 来保护端点:
- JWT:带有 Claims 和签名的令牌,OAuth 2.0 Resource Server 验签所需的
public_key
一般在请求认证授权服务器由其返回给 OAuth 2.0 Resource Server。 - Opaque Tokens:不透明的令牌,要验证一个不透明的令牌,令牌的接收方需要调用发出该令牌的服务器。
Spring Boot 2.x 示例
以下是微服务应用的配置示例
配置认证服务器
# 自动装配
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com/issuer
----
spring:
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: http://localhost:9080/auth/realms/ccm
registration:
keycloak:
client-id: client-id
client-secret: client-secret
前面一种是自动装配的配置方式,后一种是当与 OAuth 2.0 Login / OAuth 2.0 Client 特性共存时,使用相同的配置,此时需要手动装配 org.springframework.security.oauth2.jwt.ReactiveJwtDecoder
:
ServerHttpSecurity 配置
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
// @formatter:off
http.oauth2ResourceServer()
.jwt();
// @formatter:on
return http.build();
}
其它集成
很多的开源软件都支持集成 OAuth2.0,由于 OIDC 是 OAuth2.0 协议的超集,支持集成 OAuth2.0 的软件必定也能轻松的集成到 OIDC 认证服务,无非是是否使用 ID Token 的差别。
Grafana
Grafana 默认支持以下认证授权服务的集成:
- GitHub
- GItLab(自行部署)
- Grafana
- Azure AD:Azure Active Directory is Microsoft’s cloud-based identity and access management service
- Okta
- 传统 OAuth2.0
Grafana 集成 Keycloak
修改 grafana.ini
如下配置:
#################################### Generic OAuth #######################
[auth.generic_oauth]
name = ccmadapt
enabled = true
allow_sign_up = true
client_id = grafana
client_secret = grafana
scopes = openid email name
;email_attribute_name = email:primary
;email_attribute_path =
;role_attribute_path =
auth_url = http://192.168.7.20:9080/auth/realms/ccm/protocol/openid-connect/auth
token_url = http://192.168.7.20:9080/auth/realms/ccm/protocol/openid-connect/token
api_url = http://192.168.7.20:9080/auth/realms/ccm/protocol/openid-connect/userinfo
;allowed_domains =
;team_ids =
;allowed_organizations =
tls_skip_verify_insecure = false
;tls_client_cert =
;tls_client_key =
;tls_client_ca =
Portainer
Portainer 是一个通用的容器管理工具,它可以帮助用户部署和管理基于容器的应用程序,而不需要知道如何编写任何特定于平台的代码。
Portainer 的认证授权服务集成直接可以在管理界面修改,默认支持以下认证授权服务的集成:
- GitHub
- Azure AD:Azure Active Directory is Microsoft’s cloud-based identity and access management service
- 传统 OAuth2.0
通过在 Settings > Authentication > OAuth > Custom
修改配置项完成集成。
OAuth2.0 + OIDC 如何与开放服务进行集成?
本节内容摘抄自极客时间:OAuth 2.0实战课
为了方便理解,在接下来的讲述中,假定有这样一家叫 ACME 的新零售公司。其核心可以用一个简化的微服务架构图来描述:
己方应用 + Resource Owner Password Credentials Grant
使用用户名密码直接登录,不作深入讨论。
己方应用 + Authorization Code Flow
移动应用与 WEB 应用的差别就是,对于移动应用,业界建议使用 PKCE 扩展的授权码模式。下面以移动应用为例讲解基本流程:
- 用户访问 App,点击登录。
- App 生成 PKCE 相关的 code verifier + challenge。
- App 以内嵌方式启动手机浏览器,访问 IDP 的统一认证页,请求带上 PKCE 的 code challenge 相关参数。
- IDP 返回统一认证页。
- 用户认证和授权。
- IDP 通过 Login Service 对用户进行认证。
- IDP 返回授权码到 App 浏览器。
- App 截取浏览器带回的授权码,将授权码 + PKCE code verifer,通过网关转发到 IDP 的令牌获取端点。
- IDP 校验 PKCE 和授权码,校验通过则返回有效访问令牌。
- App 获取令牌,本地存储,登录成功。
之后,App 如果需要和后台交互,可直接通过网关调用后台微服务,请求 HTTP header 中带上 OAuth 2.0 访问令牌即可。
第三方 Web 应用 + Authorization Code Flow
该场景是某第三方合作厂商开发了一个 Web 网站,要访问 ACME 公司的电商开放平台 API。这是一个第三方 Web 应用场景,通常选用 OAuth 2.0 的授权码许可模式。
- 用户访问这个第三方 Web 应用,点击登录链接。
- Web 应用后台向 ACME 公司的 IDP 服务发送申请授权码请求。
- 用户被重定向到 ACME 公司的 IDP 统一登录页面。
- 用户进行认证和授权。
- IDP 通过 Login Service 对用户进行认证。
- 认证和授权通过,IDP 返回授权码。
- Web 应用获得授权码,再向 IDP 服务的令牌获取端点发起请求。
- IDP 校验授权码,校验通过则返回有效 OAuth 2.0 令牌(根据需要也可以返回刷新令牌)。
- Web 应用创建用户 Session,将 OAuth 2.0 令牌保存在 Session 中,然后返回登录成功到用户端。
- 用户浏览器中记录 Session Cookie,登录成功,Cookie 中包含的是 Sesssion ID,而不是令牌。
之后,第三方 Web 应用如果需要和 ACME 电商平台交互,可直接通过网关调用微服务,请求 HTTP header 中带上 OAuth 2.0 访问令牌即可。
其它说明
关于 IDP 和网关的部署方式
前面的几张架构图中,IDP 虽然躲在网关后面,但实际上 IDP 可以直接通过 Nginx 对外暴露,不经过网关。或者,IDP 的登录授权页面,可以通过 Nginx 直接暴露,API 接口则走网关。
对于开放服务,网关之前的安全机制主要基于 OAuth 2.0 访问令牌实现,网关之后的安全机制主要基于 JWT 令牌实现。网关层在中间实现两种令牌的转换。这是一种 OAuth 2.0 访问令牌 +JWT 令牌的混合模式。
关于浏览器、客户端和 Web Session
出于安全的考虑,不要使用浏览器作为 OAuth 2.0 Client,因为客户端要持有 client-secret
,并且要暂时保存 Access Token、ID Token 等敏感数据,上述的所有案例中,浏览器都是作为用户代理而非大家理解的客户端。
浏览器与 OAuth 2.0 Client 之间一般采用 Cookies 和 Session 保持传话,OAuth 2.0 Client 与 OAuth 2.0 Resource Server 使用 JWT 格式的 Access Token 作为凭证。
各大开放平台是如何使用 OAuth 2.0 的?
- 当有多个受保护资源服务的时候,基本的鉴权工作,包括访问令牌的验证、第三方软件应用信息的验证都应该抽出一个 API 网关层,并把这些基本的工作放到这个 API 网关层。
- 各大开放平台都是推荐使用授权码许可流程,无论是网页版的 Web 应用程序,还是移动应用程序。
- 对于第三方软件开发者重点关注的参数,可以从授权服务的授权端点和令牌端点来区分,授权端点重点是授权码请求和响应的处理,令牌端点重点是访问令牌请求和响应的处理。
微信
支付宝
美团
实践 OAuth 2.0 时,使用不当可能会导致哪些安全漏洞?
CSRF 攻击
恶意软件让浏览器向已完成用户身份认证的网站发起请求,并执行有害的操作,就是跨站请求伪造攻击。
那如何避免这种攻击呢?方法也很简单,实际上 OAuth 2.0 中也有这样的建议,就是使用 state 参数,它是一个随机值的参数。
XSS 攻击
XSS 攻击的主要手段是将恶意脚本注入到请求的输入中,攻击者可以通过注入的恶意脚本来进行攻击行为,比如搜集数据等。
最简单的方法就是对非法信息做转义过滤,比如对包含<script>
、<img>
、<a>
等标签的信息进行转义过滤。
水平越权
水平越权是指,在请求受保护资源服务数据的时候,服务端应用程序未校验这条数据是否归属于当前授权的请求用户。
发生水平越权问题的根本原因,是开发人员的认知与意识不够。如果认知与意识跟得上,那在设计之初增加归属关系判断,比如订单 ID 和商家 ID 的归属关系判断。
授权码失窃
重定向 URI 被篡改
授权码到底是怎么失窃的呢?接下来,介绍的就是授权码失窃的可能的方法之一,这也是 OAuth 2.0 中因重定向 URI 校验方法不当而遭受到的一种危害。这种安全攻击类型,就是重定向 URI 被篡改。
解决方案是校验回调 URI,一般是基于白名单。
总结
- OIDC 使得身份认证可以作为一个服务存在
- OIDC 可以很方便的实现 SSO(跨顶级域)
- OIDC 兼容 OAuth 2.0,可以使用 Access Token 控制受保护的 API 资源
- 基于 OAuth 2.0 协议。ID Token 是经过 OAuth 2.0 流程来获取的,这个流程即支持 Web 应用,也支持原生 App
- OIDC 可以兼容众多的 IDP 作为 OIDC 的 OP 来使用
- OIDC 足够简单。但同时也提供了大量的功能和安全选项以满足企业级业务需求
- OIDC 的一些敏感接口均要求 TLS,除此之外,得益于 JWT,JWS,JWE 家族的安全机制,使得一些敏感信息可以进行数字签名、加密和验证,进一步确保整个认证过程中的安全保障