快速理解OIDC

OIDC,全称为OpenID Connect,是基于OAuth 2.0 的身份认证协议。在理解OIDC之前,需要先搞清楚OAuth 2.0的相关定义。

认证和授权

首先,我们需要区分Authentication和Authorization,即认证和授权。

  1. Authentication(认证),指的是身份验证,验证“你是谁”的过程。

  2. Authorization(授权),指的是权限授予,验证“你能访问什么”的过程。

举个生活中的例子,当我们需要登机时,需要出示登机牌和机票。登机牌证明了我们的身份,机票证明了我们可以乘坐什么舱位。在实际的使用场景中,Authentication和Authorization是相互依赖的,了解了这两个概念,有助于我们了解OIDC的相关概念。

OAuth2.0是一个授权协议,并不能解决身份认证的问题。

OIDC才是认证协议,真正完成了单点登录。

密码和令牌

假设如下场景:

  1. 当用户张三需要访问他的邮箱系统的时候,邮箱系统需要校验张三的用户名和密码。

  2. 假如说另一个业务系统OA也想代替张三来代收邮件。

那么问题就来了,在OAuth2出现之前,通常的做法就是张三把他的密码share给这个OA系统。 这么做存在严重的安全隐患:

  1. 如何控制OA系统的访问权限,如何不让OA系统以张三的名义随便发邮件?

  2. 如何回收OA系统访问邮件的权限?

OAuth2就是为了解决这个问题而诞生的,OAuth2引入了”授权“以及access_token的概念。

令牌(token)和密码(password)的作用都能够让OA系统访问邮箱系统,但他们有以下几点差异:

  1. token是短期的,到期自动失效,用户自己无法修改。密码一般长期有效,用户不修改,就不会发生变化。

  2. 令牌可以被数据所有者撤销,会立即失效。

  3. 令牌有权限范围(scope),比如只能接收邮件,不能发送邮件。对于网络服务来说,只读令牌就比读写令牌更安全。密码一般是完整权限。

OAuth2.0的授权类型

根据 RFC 6749,OAuth 2.0 对于如何颁发令牌的细节,规定得非常详细,分成以下几种授权类型(authorization grant)。

授权码模式(Authorization Code)

  1. 用户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参数表示要求的授权范围

  1. 用户跳转后,IDP网站会要求用户登录,然后询问是否同意给予SP网站授权。用户表示同意之后,这时IDP网站就会跳回redirect_uri参数指定的网址。跳转时,会传回一个授权码。
1https://www.sp.com/callback?code=AUTHORIZATION_CODE
  1. 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/callback
8
9 client_id=CLIENT_ID&
10 client_secret=CLIENT_SECRET&
11 grant_type=authorization_code&
12 code=AUTHORIZATION_CODE&
13 redirect_uri=CALLBACK_URL
14}'

OAuth一般使用Basic Auth的方式传入client_id和client_secret。

但也支持将client_id和client_secret放入form表单。此处特地写了两遍,并不代表实际使用中都需要传,以下几种授权类型与此相同,不再赘述。

  1. IDP服务端验证成功后,会返回如下response。
1HTTP/1.1 200 OK
2Content-Type: application/json
3{
4 "access_token": "xxxxxxx",
5 "refresh_token": "xxxxxxxxx",
6 "token_type": "Bearer",
7 "expires_in": 3600
8}

简化模式(Implicit)

implicit grant type跳过了授权码的过程,直接返回accessToken。所有步骤在浏览器中完成,令牌对访问者是可见的

  1. 同授权码模式,发起authorize请求。
1https://www.idp.com/oauth/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=CALLBACK_URL&scope=read
  1. IDP网址验证权限之后,直接携带token重定向回SP网站
1https://www.sp.com/callback#access_token=2YotnFZFEjr1zCsicMWpAA&token_type=Bearer&expires_in=3600

注意,这里是#,不是?

密码模式(Password)

  1. 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=PASSWORD
8}'
  1. IDP服务端会返回如下response。
1HTTP/1.1 200 OK
2Content-Type: application/json
3{
4 "access_token": "xxxxxxx",
5 "refresh_token": "xxxxxxxxx",
6 "token_type": "Bearer",
7 "expires_in": 3600
8}

客户端模式(Client Credentials Grant)

一般适用于没有界面的命令行应用

  1. 请求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_credentials
6}'
  1. IDP服务端会返回如下response。
1HTTP/1.1 200 OK
2Content-Type: application/json
3{
4 "access_token": "xxxxxxx",
5 "refresh_token": "xxxxxxxxx",
6 "token_type": "Bearer",
7 "expires_in": 3600
8}

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

  1. 用户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

  1. IDP登录成功之后,重定向回来,携带一个授权码。
1https://www.sp.com/callback?code=AUTHORIZATION_CODE
  1. 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/callback
8
9 client_id=CLIENT_ID&
10 code_verifier=CODE_VERIFY&
11 grant_type=authorization_code&
12 code=AUTHORIZATION_CODE&
13 redirect_uri=CALLBACK_URL
14}'

这里同授权码模式,一般使用Basic Auth的方式传入client_id和code_verify,详见备注1。

但也支持将client_id和code_verify放入form表单。此处特地写了两遍,并不代表实际使用中都需要传。

  1. IDP服务端验证成功后,会返回如下response。
1HTTP/1.1 200 OK
2Content-Type: application/json
3{
4 "access_token": "xxxxxxx",
5 "refresh_token": "xxxxxxxxx",
6 "token_type": "Bearer",
7 "expires_in": 3600
8}

服务端将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": true
9}