OIDC,即OpenID Connect,它是基于OAuth 2.0的认证授权行业标准协议。它构建在OAuth 2.0之上,提供了一个身份认证层,用于用户身份的认证。OIDC允许用户数据安全地暴露给第三方,同时支持各种类型的客户端,如服务端应用、移动应用和Web应用。使用OIDC时,授权服务器会为第三方客户端提供用户的身份认证信息,确保了用户身份的验证和授权过程。OIDC与OAuth 2.0是完全兼容的,因此,部署了OIDC服务的系统也可以作为OAuth 2.0的服务来使用。
概述
当你的用户需要访问服务器上受保护资源的时候,例如用户订单数据,用户购物车等,需要携带id_token。
id_token是用户的身份标识,就像身份证一样。id_token是一个JWT Token
。
如果请求没有携带id_token,或者携带的id_token
不合法,则表示本次请求认证不通过。
你为其他人提供身份服务,其他应用需要从你的服务器获取用户信息。
比如github提供身份服务,其他应用可以向github索要用户的信息,完成用户在他们平台的注册,即三方登录。其实质是,在用户同意第三方应用访问该用户在授权服务器上的信息的前提下,授权服务器生成一个Access Token给第三方,只要第三方拥有这个token,就能代表用户访问授权服务器上的用户身份信息。
OIDC流程
既然需要认证、授权,那你的服务器,第三方服务器、用户这三者之间如何通信呢?如何颁发token?需要遵守怎样的规范?OIDC就是这样一套规范,其应用广泛,轻量简单,基于OAuth 2.0,既可以用于授权,也可以用于身份认证。
access_token用于授权,id_token用户认证。
授权码模式
授权码模式是最常用的OIDC流程,如下图所示:
描述如下:
- 第三方发起授权请求
- 授权服务器询问用户是否同意授权,要求用户输入用户名和密码。
- 授权服务器返回一个授权码给第三方应用(前端或后端都行)。
- 第三方应用后端携带这个授权码向授权服务器请求token。
- 授权服务器返回id_token和access_token。
在上述流程中,授权请求里面的scope参数中是带有openid的,因此最后返回的token包含id_token。如果不带openid呢?则最后返回的只有accessToken,而没有id_token。
隐式模式
当发起授权请求时query参数中response_type不为code的时候,则表示使用隐式模式。
在这种模式下,response_type可以为token
或id_token
或id_token token
。
隐式模式可以在一次请求中就获取到token,而不需要额外的返回授权码再通过授权码获取token这两步。返回的token中包含什么则取决于response_type,可以只有access token或者只有id_token或者两者都包含。
隐式模式流程如下:
可以看出,隐式模式最为简单直接,但是在安全性上不如授权码模式,因为可能会在前端暴露Access Token。隐式模式要求回调地址必须为https。
从流程图可以看出,当授权服务器要求用户授权后,授权服务器需要主动回调客户端。所谓的回调地址就用在这里。
混合模式
混合模式的意思是发起授权请求后,既返回授权码又返回AccessToken或ID Token。
混合模式要求query参数response_type=code id_token
或response_type=code token
或response_type=code id_token token
。
这三种形式的混合模式流程如下所示:
response_type是第一次用户认证之后的返回值,scope是获取用户信息时的返回值。
授权码模式的安全性
OIDC授权码模式的认证流程中涉及三方:用户、OIDC服务器(OP, OIDC Provider)、应用服务器(SP, Service Provider)。
SP、用户、OP的交互目的分为以下几点:
- SP希望拿到一个可信的身份断言,从而让用户登录。
- SP发起登录,会跳转到OP的认证页面,OP让用户登录,并授权自己的信息,然后OP将一个授权码code发给SP。
- SP收到授权码之后,结合Client ID和Client Secret 到OP换取该用户的access_token。
- SP利用access_token到OP去获取用户的相关信息,而从得到一个可信的身份断言,让用户登录。
OIDC协议中,用户登录成功后,OIDC认证服务器会将用户的浏览器回调到一个回调地址,并携带一个授权码code。这个授权码一般有效期10分钟且一次有效,用后作废。
后端收到授权码code之后,需要使用Client Id + Client Secret + Code 去OIDC认证服务器换取用户的access_token。在这一步,实际上OIDC Server对OAtuth Client进行了认证,能够确保来OIDC认证服务器获取access_token的机器是可信任的,而不是任何一个人拿到code之后都能来OIDC认证服务器换取token。即使code被黑客获取到,如果他没有Client Id + Client Secret也无法使用。就算有,也要和真正的应用服务器竞争,因为code一次有效,用后作废,加大了攻击难度。
授权请求参数
在上文中经常提到scope和response_type参数,事实上,授权请求中有如下Query Parameters参数:
参数名 | 类型 | 说明 |
---|---|---|
client_id(必填) | string | 应用id |
redirect_uri(必填) | sting | 回调地址,授权服务器返回授权码时使用 |
scope(必填) | sting | 指定客户端请求token时所需权限,如果是授权码模式则必须包含openid;还可以指定profile和offline_access ,分别表示要获取用户基本文件信息和获取刷新令牌;多个scope参数用空格分隔。id_token解码后的内容会包括scope对应的用户信息相关字段 |
response_type(必填) | sting | 标识登录成功后OP要返回的信息 |
prompt(可选) | sting | 可以为 none,login,consent 或 select_account ,指定 OP 与 End-User 的交互方式,如需 refresh_token ,必须为 consent |
state(必填) | string | 一个随机字符串,用于防范CSRF攻击,如果response中的state值和请求发送之前设置的state不同,则说明受到攻击 |
nonce(可选) | string | 一个随机字符串,用于防范Replay攻击 |
通过授权码获取访问令牌accessToken时,通常需要如下参数:
参数名 | 类型 | 说明 |
---|---|---|
grant_type(必填) | string | 指定授权类型为 “authorization_code”,表示使用授权码进行访问令牌的获取 |
code(必填) | sting | 授权码 |
redirect_uri(必填) | sting | 回调地址,与授权请求时提供的回调地址必须一致 |
client(必填) | sting | 客户端标识符 |
client_secret(可选) | sting | 客户端密钥 |
Token验证
在上文中介绍了几种从授权服务器获取OIDC token的方式。但拿到token后,一般来说,还需要校验token的合法性,以确保token未被篡改、未过期,并且是由可信的授权服务器签发,主要验证签名和声明。
首先需要将token转换成JWT,其中包含了签名,key id,jwt的签发者,audience和过期时间等。
验证签名时需要从远程获取Json Web Key Set
,从该keySet中获取key id对应的JWK,再根据签名算法来构建签名验证器,最后调用JWT的verify方法来验证签名。
除了签名之外,还需要验证token没有过期,token签发者和audience等是合法的。下面是代码示例:
// 先将字符串类型的Json Web Token 转换成签名的token
SignedJWT signedJWT = SignedJWT.parse(token);
// 获取远程Json Web Key Set
RemoteJWKSet<JWKSecurityContext> remoteJWKSet = new RemoteJWKSet<>(new URL("https://example.com/.well-known/jwks.json"));
// 从signedJWT中获取key的标识
String kid = signedJWT.getHeader().toJSONObject().get(Claim.KEY_ID.getValue()).toString()
// 从远程JWKS中获取JWK
JWKMatcher jwkMatcher = new JWKMatcher.Builder().keyID(kid).build();
JWK jwk = remoteJWKSet.get(new JWTSelector(jwkMatcher), null).stream().findFirst();
// 构建JWS验证器
JWSVerifier verifier = new RSASSAVerifier(RSAKey.parse(jwk.toJSONString()).toRSAPublicKey());
// 验证签名、有效期和声明
bool result = signedJWT.verify(verifier) && signedJWT.getJWTClaimsSet().getExpirationTime().toInstant().isAfter(Instant.now())
&& Objects.equals("myIssuer", signedJWT.getJWTClaimsSet().getIssuer())
&& signedJWT.getJWTClaimsSet().getAudience().contains(CLIENTID);
参考资料
[1]https://old-docs.authing.cn/authentication/oidc/understand-oidc.html
文章评论