快速理解OIDC
OIDC,全称为OpenID Connect,是基于OAuth 2.0 的身份认证协议。在理解OIDC之前,需要先搞清楚OAuth 2.0的相关定义。
认证和授权
首先,我们需要区分Authentication和Authorization,即认证和授权。
-
Authentication(认证),指的是身份验证,验证“你是谁”的过程。
-
Authorization(授权),指的是权限授予,验证“你能访问什么”的过程。
举个生活中的例子,当我们需要登机时,需要出示登机牌和机票。登机牌证明了我们的身份,机票证明了我们可以乘坐什么舱位。在实际的使用场景中,Authentication和Authorization是相互依赖的,了解了这两个概念,有助于我们了解OIDC的相关概念。
OAuth2.0是一个授权协议,并不能解决身份认证的问题。
OIDC才是认证协议,真正完成了单点登录。
密码和令牌
假设如下场景:
-
当用户张三需要访问他的邮箱系统的时候,邮箱系统需要校验张三的用户名和密码。
-
假如说另一个业务系统OA也想代替张三来代收邮件。
那么问题就来了,在OAuth2出现之前,通常的做法就是张三把他的密码share给这个OA系统。 这么做存在严重的安全隐患:
-
如何控制OA系统的访问权限,如何不让OA系统以张三的名义随便发邮件?
-
如何回收OA系统访问邮件的权限?
OAuth2就是为了解决这个问题而诞生的,OAuth2引入了”授权“以及access_token的概念。
令牌(token)和密码(password)的作用都能够让OA系统访问邮箱系统,但他们有以下几点差异:
-
token是短期的,到期自动失效,用户自己无法修改。密码一般长期有效,用户不修改,就不会发生变化。
-
令牌可以被数据所有者撤销,会立即失效。
-
令牌有权限范围(scope),比如只能接收邮件,不能发送邮件。对于网络服务来说,只读令牌就比读写令牌更安全。密码一般是完整权限。
OAuth2.0的授权类型
根据 RFC 6749,OAuth 2.0 对于如何颁发令牌的细节,规定得非常详细,分成以下几种授权类型(authorization grant)。
授权码模式(Authorization Code)
- 用户SP网站点击后就会跳转到IDP网站,授权用户数据给SP网站使用。下面就是SP网站跳转IDP网站的一个示意链接。
1https://www.idp.com/oauth/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=CALLBACK_URL&scope=read
-
response_type
参数表示要求返回授权码(code
) -
client_id
参数让认证源知道是哪个系统在请求用户信息 -
redirect_uri
参数是IDP网站接受或拒绝请求后的跳转网址 -
scope
参数表示要求的授权范围
- 用户跳转后,IDP网站会要求用户登录,然后询问是否同意给予SP网站授权。用户表示同意之后,这时IDP网站就会跳回
redirect_uri
参数指定的网址。跳转时,会传回一个授权码。
1https://www.sp.com/callback?code=AUTHORIZATION_CODE
- SP网站拿到授权码以后,在服务端向IDP网站请求令牌token。
这里一定要由服务端发起请求,CLIENT_SECRET不能由前端页面获知
1curl \2 -X POST https://www.idp.com/oauth/token \3 -H 'Content-Type:application/x-www-form-urlencoded' \4 -H 'Authorization:Basic {CLIENT_ID: CLIENT_SECRET}' \5 -d '{grant_type=authorization_code&6 code={code}&7 redirect_uri=https://www.sp.com/callback89 client_id=CLIENT_ID&10 client_secret=CLIENT_SECRET&11 grant_type=authorization_code&12 code=AUTHORIZATION_CODE&13 redirect_uri=CALLBACK_URL14}'
OAuth一般使用Basic Auth的方式传入client_id和client_secret。
但也支持将client_id和client_secret放入form表单。此处特地写了两遍,并不代表实际使用中都需要传,以下几种授权类型与此相同,不再赘述。
- IDP服务端验证成功后,会返回如下response。
1HTTP/1.1 200 OK2Content-Type: application/json3{4 "access_token": "xxxxxxx",5 "refresh_token": "xxxxxxxxx",6 "token_type": "Bearer",7 "expires_in": 36008}
简化模式(Implicit)
implicit grant type跳过了授权码的过程,直接返回accessToken。所有步骤在浏览器中完成,令牌对访问者是可见的
- 同授权码模式,发起authorize请求。
1https://www.idp.com/oauth/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=CALLBACK_URL&scope=read
- IDP网址验证权限之后,直接携带token重定向回SP网站
1https://www.sp.com/callback#access_token=2YotnFZFEjr1zCsicMWpAA&token_type=Bearer&expires_in=3600
注意,这里是#,不是?
密码模式(Password)
- Resource Owner Password Credentials Grant,此模式跳过了浏览器authorize过程,直接请求token,发送用户名密码。
1curl \2 -X POST https://www.idp.com/oauth/token \3 -H 'Content-Type:application/x-www-form-urlencoded' \4 -H 'Authorization:Basic {CLIENT_ID: CLIENT_SECRET}' \5 -d '{grant_type=password&6 username=USERNAME&7 password=PASSWORD8}'
- IDP服务端会返回如下response。
1HTTP/1.1 200 OK2Content-Type: application/json3{4 "access_token": "xxxxxxx",5 "refresh_token": "xxxxxxxxx",6 "token_type": "Bearer",7 "expires_in": 36008}
客户端模式(Client Credentials Grant)
一般适用于没有界面的命令行应用
- 请求token
1curl \2 -X POST https://www.idp.com/oauth/token \3 -H 'Content-Type:application/x-www-form-urlencoded' \4 -H 'Authorization:Basic {CLIENT_ID: CLIENT_SECRET}' \5 -d '{grant_type=client_credentials6}'
- IDP服务端会返回如下response。
1HTTP/1.1 200 OK2Content-Type: application/json3{4 "access_token": "xxxxxxx",5 "refresh_token": "xxxxxxxxx",6 "token_type": "Bearer",7 "expires_in": 36008}
PKCE模式
授权码模式看起来很不错,功能完整,流程严密。但是它建立在一个前提下,服务器的client_secret不会被泄露。
当某些客户端请求时,会引发一些安全问题
- Native 原生应用
客户端应用,如安卓应用,本身源码是可以反编译的,client_secret很容易泄露。
- 单页面应用(Single Web Application,SPA)
单页应用整个源都在浏览器里,无法安全存储client_secret。
于是,引入了新的模式,PKCE模式(Proof Key for Code Exchange)。根据最新的协议安全建议,SPA不再推荐使用传统的implicit模式,而是推荐使用较为安全的PKCE模式。PKCE和授权码模式的区别是,不存在client_secret的概念。
SP应用需要先生成一个code_verifier,将其urlEncodeSHA256一下,得到code_challenge
- 用户SP网站点击后就会跳转到IDP网站
1https://www.idp.com/oauth/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=CALLBACK_URL&scope=read&code_chanllenge=CODE_CHANLLENGE
注意这里相较于授权码模式,多了一个CODE_CHANLLENGE
- IDP登录成功之后,重定向回来,携带一个授权码。
1https://www.sp.com/callback?code=AUTHORIZATION_CODE
- SP网站拿到授权码以后,在服务端向IDP网站请求令牌token。
1curl \2 -X POST https://www.idp.com/oauth/token \3 -H 'Content-Type:application/x-www-form-urlencoded' \4 -H 'Authorization:Basic {CLIENT_ID: CODE_VERIFY}' \5 -d '{ grant_type=authorization_code&6 code={code}&7 redirect_uri=https://www.sp.com/callback89 client_id=CLIENT_ID&10 code_verifier=CODE_VERIFY&11 grant_type=authorization_code&12 code=AUTHORIZATION_CODE&13 redirect_uri=CALLBACK_URL14}'
这里同授权码模式,一般使用Basic Auth的方式传入client_id和code_verify,详见备注1。
但也支持将client_id和code_verify放入form表单。此处特地写了两遍,并不代表实际使用中都需要传。
- IDP服务端验证成功后,会返回如下response。
1HTTP/1.1 200 OK2Content-Type: application/json3{4 "access_token": "xxxxxxx",5 "refresh_token": "xxxxxxxxx",6 "token_type": "Bearer",7 "expires_in": 36008}
服务端将code_verify同样进行urlEncodeSHA256处理,与之前接收的code_chanllenge进行比对。
OIDC
我们前面提到了认证和授权,注意,OAuth协议只是一个授权协议,不涉及到身份认证。
OIDC在OAuth2.0的基础上,添加了身份认证的一层。
OIDC增加了以下内容
-
token接口增加了id_token的返回:在获取token接口,除了返回access_token和refresh_token,还需要返回id_token,id_token是一个JWT格式的token,里面携带用户信息。
-
增加了user_info接口:使用access_token可以调用user_info接口获取用户信息。
-
定义了well-known接口:即OIDC的描述文件。
id_token
在OIDC的规范里,详细定义了id_token的格式,必须为JWT格式。
OIDC服务端需要提供一个RSA的公钥,供客户端验证JWT的签名。
id_token里携带了用户基本信息,客户端可以直接使用id_token里的信息,也可以使用access_token调用user_info接口获取用户信息
user_info接口
客户端除了使用id_token标识用户以外,还可以额外调用user_info接口,获取用户信息
1curl \2 -X GET https://www.idp.com/oauth/user_info \3 -H 'Content-Type:application/json ' \4 -H 'Authorization:Bearer {access_token}' \
此处使用Bearer Token的方式,详见备注2。
OIDC客户端,返回的用户信息的属性是严格规范的
1{2 "sub": "us-xxxx",3 "name": "张三",4 "preferred_username": "zhangsan",5 "email": "zhangsan@test.com",6 "email_verified": true,7 "phone_number": "123456789001",8 "phone_number_verified": true9}