Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

OpenID Connect

21
Sep
2019

OpenID Connect

By Alex
/ in Work
0 Comments
OAuth 2.0

OAuth是关于授权(给予第三方应用访问你存放在某处的资料,例如让Gmem访问你的Github头像)的开放标准,目前版本是2.0。

只有得到用户的授权,Github才会允许Gmem访问你的头像。那么Github怎么样才能确认Gmem获得授权了呢?最简单的方法是把你的Github密码告诉Gmem,这显然是不安全的:

  1. 你必须修改密码,才能取消授权
  2. 如果Gmem泄漏了密码,你的Github账户将发生危险

OAuth协议就是为了解决这个安全问题而生。

名词术语
术语 说明
Third-party App 第三方应用程序,OAuth的客户端,例如上面例子中的Gmem
HTTP service 服务提供商,例如上面例子中的Github
Resource Owner 资源所有者,用户,例如上面例子中的Github用户
User Agent 用户代理,通常是浏览器
Authorization server 专门处理授权请求的服务
Resource server 专门维护资源的服务,可以和Authorization server是同一服务,也可以是不同服务

OAuth提供一种机制,让Third-party App安全的获得User Agent的授权,然后和HTTP service交互。

工作原理

OAuth在第三方应用和服务提供商之间,加入一个授权层(Authorization layer),第三方应用不直接登陆到服务提供商,而是登陆授权层。授权层给予第三方APP以令牌,令牌包含权限范围、有效期等关键信息。

第三方应用携带令牌,访问服务提供商,后者校验令牌并确定它能否访问目标资源。

OAuth协议的抽象工作流图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
     +--------+                               +---------------+
     |        |--(A)- Authorization Request ->|   Resource    |
     |        |                               |     Owner     |
     |        |<-(B)-- Authorization Grant ---|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(C)-- Authorization Grant -->| Authorization |
     | Client |                               |     Server    |
     |        |<-(D)----- Access Token -------|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(E)----- Access Token ------>|    Resource   |
     |        |                               |     Server    |
     |        |<-(F)--- Protected Resource ---|               |
     +--------+                               +---------------+

其中A、B表示第三方APP获得资源所有者的授权的过程,有多种变体,对应了OAuth的授权模式。

授权模式

客户端必须得到用户的授权(Authorization Grant),才能获得令牌(Access Token)。OAuth 2.0定义了四种授权方式:

  1. 授权码模式(authorization code)
  2. 简化模式(implicit)
  3. 密码模式(resource owner password credentials)
  4. 客户端模式(client credentials)
授权码模式

该模式功能最完整、流程最严密,它让第三方应用的后台,与授权服务进行交互:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
     +----------+
     | Resource |
     |   Owner  |
     |          |
     +----------+
          ^
          |
         (B)
     +----|-----+          Client Identifier      +---------------+
     |         -+----(A)-- & Redirection URI ---->|               |
     |  User-   |                                 | Authorization |
     |  Agent  -+----(B)-- User authenticates --->|     Server    |
     |          |                                 |               |
     |         -+----(C)-- Authorization Code ---<|               |
     +-|----|---+                                 +---------------+
       |    |                                         ^      v
      (A)  (C)                                        |      |
       |    |                                         |      |
       ^    v                                         |      |
     +---------+                                      |      |
     |         |>---(D)-- Authorization Code ---------'      |
     |  Client |          & Redirection URI                  |
     |         |                                             |
     |         |<---(E)----- Access Token -------------------'
     +---------+       (w/ Optional Refresh Token)
 
   Note: The lines illustrating steps (A), (B), and (C) are broken into
   two parts as they pass through the user-agent.
 
                     Figure 3: Authorization Code Flow

步骤如下:

  1. 用户访问第三方应用,后者将前者导向授权服务器,附带一个重定向URL(A)。导向授权服务器时,附带以下URL参数:
    1. response_type:表示授权类型,必须,此处的值固定为"code"
    2. client_id:表示客户端的ID,必须
    3. redirect_uri:表示重定向URI,可选
    4. scope:表示申请的权限范围,可选
    5. state:表示客户端的当前状态,可以指定任意值,授权服务器会原样返回
  2. 认证服务器会询问资源所有者(通常就是上条的用户,坐在浏览器面前),是否授予权限(B)
  3. 如果资源所有者点击允许,则认证服务器执行浏览器重定向,回到步骤A指定的URL,并且附带授权码(C)。授权服务器进行重定向时,在URL后附加参数:
    1. code:表示授权码,必须。授权码的有效期应该很短,例如10分钟,第三方应用只能使用该码一次,否则会被授权服务器拒绝。授权码和client_id + redirect_url的组合是一一对应的
    2. state:如果客户端的请求中包含这个参数,授权服务器原样返回
  4. 第三方应用的后台,使用授权码,向授权服务器申请访问令牌(D)。授权服务器的应答包含以下参数:
    JSON
    1
    2
    3
    4
    5
    6
    7
    {
      "access_token":"2YotnFZFEjr1zCsicMWpAA", // 令牌
      "token_type":"bearer",  // 令牌类型,例如bearer、mac
      "expires_in":3600, // 令牌有效期,单位秒
      "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", // 用于刷新令牌
      "scope":"..."  // 授权范围,如果和申请时一致,可省略
    }
  5. 授权服务器向第三方应用的后端发送访问令牌(E),此令牌第三方后端自己保存,不发给前端
简化模式

该模式下,不需要经过第三方应用的后端,直接在浏览器中申请令牌,跳过授权码这个步骤。令牌对用户可见。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
     +----------+
     | Resource |
     |  Owner   |
     |          |
     +----------+
          ^
          |
         (B)
     +----|-----+          Client Identifier     +---------------+
     |         -+----(A)-- & Redirection URI --->|               |
     |  User-   |                                | Authorization |
     |  Agent  -|----(B)-- User authenticates -->|     Server    |
     |          |                                |               |
     |          |<---(C)--- Redirection URI ----<|               |
     |          |          with Access Token     +---------------+
     |          |            in Fragment
     |          |                                +---------------+
     |          |----(D)--- Redirection URI ---->|   Web-Hosted  |
     |          |          without Fragment      |     Client    |
     |          |                                |    Resource   |
     |     (F)  |<---(E)------- Script ---------<|               |
     |          |                                +---------------+
     +-|--------+
       |    |
      (A)  (G) Access Token
       |    |
       ^    v
     +---------+
     |         |
     |  Client |
     |         |
     +---------+
 
   Note: The lines illustrating steps (A) and (B) are broken into two
   parts as they pass through the user-agent.
 
                       Figure 4: Implicit Grant Flow

步骤如下:

  1. 第三方应用将用户导向授权服务器
  2.  用户决定是否给于第三方应用授权
  3. 如果用户给予授权,授权服务器将用户导向第三方应用指定的重定向URL,并在URL的Hash部分包含了访问令牌
  4. 浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值
  5. 资源服务器返回一个脚本,其中包含的代码可以获取Hash值中的令牌
  6. 浏览器执行上一步获得的脚本,提取出令牌
  7. 浏览器将令牌传递给第三方应用客户的
密码模式

在此模式下,用户向第三方应用提供自己的用户名和密码。第三方应用使用这些信息,向服务商提供商索要授权。

客户端模式

第三方应用以自己的身份,通过授权服务器,向服务提供商索取授权。

OIDC

OpenID Connect目前已经成为单点登陆(single sign-on)、身份提供(identity provision)的领先标准。它的特点是:

  1. 简单的基于JSON的身份令牌(identity tokens) —— JWT,JWT简单而具有可移植性(自我验证),支持大量签名算法、加密算法
  2. 衍生自OAuth 2.0的工作流,对Web应用、基于Web的/原生的移动应用友好

OpenID Connect是2014年发布的标准,它不是IdP的第一个标准,但是却是最简单、好用的。

为什么要引入认证服务

应用程序通常都需要识别(identify)其用户,简单的方式是,使用本地数据库,存储用户账户、凭证信息。这种方式的缺点是:

  1. 注册、登陆很繁琐,导致用户反感
  2. 具有很多应用的企业,维护多套用户信息很痛苦

解决方案就是引入独立的认证服务器,将身份验证(authentication)、用户创建(provisioning)的任务交给它统一处理。此认证服务器叫做Identity Provider(IdP)。

Github、Google、Wechat等互联网服务,都扮演IdP的身份。你的应用可以直接与之集成,有它们提供登陆功能。

ID Token用途

除了用于单点登陆之外,ID Token还可以用于:

  1. 无状态会话:将ID Token放入Cookie,可以实现简单的轻量级stateless session,而不需要服务器端记录任何信息。服务器通过检查ID Token校验会话有效性
  2. 将身份信息传递给第三方应用
  3. Token exchange:在那些需要出示你的身份,以获得访问的场景。例如在酒店Check in的时候获取房间钥匙
ID Token格式

代表一个身份,标准的JWT格式,被OpenID Provider(OP)签名。

为了获得ID Token,第三方应用必须将终端用户导向(OP)提供的界面,进行身份验证操作,类似OAuth那样。

ID Token中包含以下字段:

  1. 对用户身份的断言,用户身份在OpenID中叫做subject,也叫sub
  2. 颁发此Token的权威机构(就是OP),issuing authority,也叫iss
  3. 是否此Token仅限于特定应用(客户端)使用,audience,也叫aud
  4. 可以包含一个nonce
  5. 颁发时间iat、过期时间(exp)
  6. 可以包含请求的关于subject的额外信息,例如名字、电子邮件地址
  7. 签名信息
  8. 可以加密

这些字段,也叫statements / claims,打包在JSON中:

JSON
1
2
3
4
5
6
7
8
9
10
{
  "sub"       : "alice",
  "iss"       : "https://openid.c2id.com",
  "aud"       : "client-12345",
  "nonce"     : "n-0S6_WzA2Mj",
  "auth_time" : 1311280969,
  "acr"       : "c2id.loa.hisec",
  "iat"       : 1311280970,
  "exp"       : 1311281970
}

ID Token头部 + claims JSON + 签名,被一起Base64编码,便于基于HTTP进行传递。 

获取ID Token

第三方应用(客户端,Relying Party,RP)要获取ID Token,必须通过IdP,在IdP那里,终端用户的会话/凭证被检查。为了将终端用户引导到IdP那里,需要一个可靠的User Agent,这个角色通常由浏览器扮演:

  1. 对于Web应用,可能是弹出对话框,在此对话框中重定向到IdP
  2. 对于Native应用,客户端可能需要启动系统浏览器,因为内嵌的Web view不被信任(防止Native应用嗅探密码)

OIDC标准没有指定用户身份以何种方式确认,这完全由IdP自由决定,最简单的方式是基于口令。

请求ID Token的流程,遵循OAuth 2.0协议。可以使用:

  1. 授权码模式:最常使用,兼容传统Web应用、原生应用、移动应用。最安全,因为浏览器/第三方客户端不会看到ID Token。流程概要:
    1. 浏览器到OP、从OP重定向回来,实现身份校验,得到授权码
    2. 第三方应用的后台,使用授权码再次请求OP,获得ID token
  2. 简单模式:用于纯粹的JS应用,没有后端服务的那种。ID Token直接从OP的重定向中返回给客户端
  3. 混合模式:很少使用,1+2组合

本节的后续内容,以授权码模式为例,阐述获取ID Token的完整流程。

第一步

本步骤的目的:

  1. 确认最终用户的身份

实现方式:前端请求,浏览器重定向,请求Authorisation Endpoint。获得授权码

首先,RP触发浏览器重定向,重定向到OP的Authorisation Endpoint:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 302 Found
Location: https://openid.c2id.com/login?
          # code表示使用授权码模式
          response_type=code
          # OAuth授权范围openid,表示请求进行OpenID身份验证,获取ID Token
          &scope=openid
          # RP在OP那里的身份标识
          &client_id=s6BhdRkqt3
          # 对于OP没有明确意义的(Qpaque)的字符串,用于维持请求/回调之间的关联性
          &state=af0ifjsldkj
          # RP的回调地质
          &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb

在OP的页面上,通常会:

  1. 检查用户是否具有合法的session
  2. 如果没有,则进入登陆页面,提示用户登陆
  3. 登陆成功后,OP通常会询问用户,是否同意登陆到RP 

随后,OP会调用/login时指定的redirect_uri,在URL中提供授权码。如果失败(登陆失败、不同意登陆到RP、回调地址不正确等)则附带一个错误码:

Shell
1
2
3
4
HTTP/1.1 302 Found
Location: https://client.example.org/cb?
          code=SplxlOBeZQQYbYS6WxSbIA
          &state=af0ifjsldkj

RP必须校验state,确保和之前的/login请求指定的一致。 

第二步

本步骤的目的:

  1. 确认第三方应用(客户端)的身份,可选
  2. 获取ID Token 

实现方式:后端请求,第三方应用的后端服务器直接访问OP。获得ID Token(+ OAuth 2.0访问令牌)。

上一步骤中,OP将授权码发给RP,发送的方式是浏览器重定向。这时候,RP前端会将授权码传给后端,由后端请求RP获得ID Token。这种做法的意义是:

  1. 获取ID Token之前,OP有机会校验RP的身份
  2. 后端获得ID Token后不会返回给前端(不可靠),不会造成敏感信息泄漏

RP后端将发起下面的请求:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
POST /token HTTP/1.1
Host: openid.c2id.com
Content-Type: application/x-www-form-urlencoded
               # RP的Client ID和Secret编码在此。注意OIDC也支持基于JWT校验RP身份
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
 
# 授权模式
grant_type=authorization_code
# 授权码
&code=SplxlOBeZQQYbYS6WxSbIA
# 重定向地址,必须和第一步中的一致
&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb

如果成功,OP将会返回一个JSON对象: 

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
 
{
  # ID Token,它是Base64编码的JWT,包含claims、签名等信息
  "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"
 
  # 不记名令牌,为了兼容OAuth 2.0协议。请求user profile data时可以用到
  "access_token": "SlAV32hkKG",
  "token_type": "Bearer",
  "expires_in": 3600,
}

其中id_token字段是Base64编码的JWT,包含claims、签名等信息。RP必须进行必要的校验。

用户信息(Claims)

OIDC规范提供了一系列标准的Claims(也就是用户属性)。 Claims提供用户的各种详细信息,例如电子邮件、头像。客户端要获取Claims,有两种方式。

第一种方式是,在/login请求的scope中,根据Claims所属的scope来获取。例如:

Shell
1
2
3
4
5
6
7
8
# 设置scope为openid email,获取ID Token + 电子邮件
HTTP/1.1 302 Found
Location: https://openid.c2id.com/login?
          response_type=code
          &scope=openid%20email
          &client_id=s6BhdRkqt3
          &state=af0ifjsldkj
          &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb

 所有Scope和Claims对应关系如下表:

Scope 关联的Claims
email email, email_verified
phone phone_number, phone_number_verified
profile name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, updated_at
address address

第二种方式是,单独使用claims参数。

Dex

dexidp/dex是一个开源项目,它是OIDC Provider、OAuth 2.0 Provider。支持可拔插的Connector。

作为身份验证服务,dex实现了框架部分,并且可以通过Connector适配到其它identity providers,这样,用户密码等信息就可以由LDAP服务器、SAML provider、AD之类的服务提供。

简介
ID Tokens

在前文中,我们了解到ID Tokens是OIDC对OAuth 2的扩展。ID Tokens是OAuth 2响应的一部分。它的格式是JWT,dex负责签名JWT,ID Tokens包含了标准的claims。

包括Kubernetes在内的第三方系统,都可以使用dex产生的ID Tokens。

dex在生产环境中的一个应用是,作为CoreOS的K8S解决方案,Tectonic的Authn加载项。

Connectors

当用户通过dex进行登陆时,用户的身份信息通常存放在第三方用户管理系统中,例如LDAP。Dex作为客户端和第三方用户管理系统之间的垫片,其价值是,客户端仅仅需要理解OIDC,后端用户管理系统可以随时切换。

Connector是Dex联系第三方系统进行用户身份验证的策略接口,取决于这些第三方系统的特性,OIDC的某些功能可能无法支持。

入门 
构建
Shell
1
2
3
go get github.com/dexidp/dex
cd $GOPATH/src/github.com/dexidp/dex
make
启动服务

Dex从一个配置文件中提取所有配置信息,阳历配置如下: 

examples/config-dev.yaml
YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# Dex的base url,也是OIDC服务的外部名称,所有客户端必须基于此URL访问Dex
# 如果提供了path部分,则dex的HTTP服务在non-root URL上监听
issuer: http://127.0.0.1:5556/dex
 
# Dex如何存储它的状态
storage:
  type: sqlite3
  config:
    file: examples/dex.db
 
  # 基于MySQL的例子
  # type: mysql
  # config:
  #   host: localhost
  #   port: 3306
  #   database: dex
  #   user: mysql
  #   password: mysql
  #   ssl:
  #     mode: "false"
 
  # 基于PostgreSQL的例子
  # type: postgres
  # config:
  #   host: localhost
  #   port: 5432
  #   database: dex
  #   user: postgres
  #   password: postgres
  #   ssl:
  #     mode: disable
 
  # 基于Etcd的例子
  # config:
  #   endpoints:
  #     - http://localhost:2379
  #   namespace: dex/
 
  # 基于K8S的例子
  # config:
  #   kubeConfigFile: $HOME/.kube/config
 
# HTTP端点配置
web:
  http: 0.0.0.0:5556
  # 可选的HTTPS配置
  # https: 127.0.0.1:5554
  # tlsCert: /etc/dex/tls.crt
  # tlsKey: /etc/dex/tls.key
 
# 遥测配置
telemetry:
  http: 0.0.0.0:5558
 
# 启用gRPC API,地址必须和HTTP不同
# grpc:
#   addr: 127.0.0.1:5557
#  tlsCert: examples/grpc-client/server.crt
#  tlsKey: examples/grpc-client/server.key
#  tlsClientCA: /etc/dex/client.crt
 
# 密码、Token超时配置
# expiry:
#   signingKeys: "6h"
#   idTokens: "24h"
 
# 日志配置
# logger:
#   level: "debug"
#   format: "text" # can also be "json"
 
# oauth2:
    # 时用 ["code", "token", "id_token"] 可以为web-only客户端启用简化模式
#   responseTypes: [ "code" ] # also allowed are "token" and "id_token"
    # 默认情况下,Dex会询问用户,是否允许和应用程序共享数据
#   skipApprovalScreen: false
    # 如果仅启用一种身份验证方式,默认行为是直接进入
    # 对于连接的IdP来说,浏览器会直接从应用程序条砖到上游IdP页面
#   alwaysShowLoginScreen: false
    # Uncommend the passwordConnector to use a specific connector for password grants
#   passwordConnector: local
 
# 静态客户端,不需要从外部存储读取
staticClients:
- id: example-app
  redirectURIs:
  - 'http://127.0.0.1:5555/callback'
  name: 'Example App'
  secret: ZXhhbXBsZS1hcHAtc2VjcmV0
 
# 上游IdP配置
connectors:
- type: mockCallback
  id: mock
  name: Example
# Google配置
# - type: google
#   id: google
#   name: Google
#   config:
#     issuer: https://accounts.google.com
#     # Connector config values starting with a "$" will read from the environment.
#     clientID: $GOOGLE_CLIENT_ID
#     clientSecret: $GOOGLE_CLIENT_SECRET
#     redirectURI: http://127.0.0.1:5556/dex/callback
#     hostedDomains:
#     - $GOOGLE_HOSTED_DOMAIN
 
# Let dex keep a list of passwords which can be used to login to dex.
enablePasswordDB: true
 
# 用于登陆的静态最终用户信息
# A static list of passwords to login the end user. By identifying here, dex
# won't look in its underlying storage for passwords.
#
# If this option isn't chosen users may be added through the gRPC API.
staticPasswords:
- email: "admin@example.com"
  # bcrypt hash of the string "password"
  hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
  username: "admin"
userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"

执行下面的命令启动Dex服务:

Shell
1
./bin/dex serve examples/config-dev.yaml
运行客户端

Dex的工作方式类似于其他OAuth 2 Provider,用户被从客户端应用重定向到Dex,进行登陆。

Dex提供了一个示例客户端:  ./bin/example-app ,这是一个Web应用,你可以打开浏览器:

  1. 打开http://localhost:5555/,进入此Web应用
  2. 点击登陆按钮,重定向到Dex
  3. 选择Login with Email,输入admin@example.com" 和 "password"进行登陆
  4. 允许Web应用的登陆请求
  5. 你可以在Web应用页面看到Token
编写客户端

应用程序和Dex的交互,可以分为两类:

  1. 请求ODIC的ID Tokens,来认证最终用户的身份,必须基于Web进行
  2. 从其它应用消费ID Token,需要验证客户端是代表用户进行操作 
初始化

客户端应该在初始化阶段,创建provider等对象:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 携带了Http客户端的Context
//                           这是一个struct{}实例
//    context.WithValue(ctx, oauth2.HTTPClient, client)
ctx := oidc.ClientContext(context.Background(), a.client)
// 使用Context中携带的客户端请求dex
//    ctx.Value(oauth2.HTTPClient).(*http.Client)
provider, err := oidc.NewProvider(ctx, issuerURL)
 
 
// 查询dex支持的Scope(Claim逻辑组)
var s struct {
    ScopesSupported []string `json:"scopes_supported"`
}
if err := provider.Claims(&s); err != nil {
    return fmt.Errorf("failed to parse provider scopes_supported: %v", err)
}
 
// 创建ID Token验证器
a.verifier = provider.Verifier(&oidc.Config{ClientID: a.clientID}) 
请求ID Token

这类用法中,应用是标准的OAuth2客户端。用户打开应用页面,应用期望知道用户的身份,做法就是导向Dex进行身份校验,然后检查得到的ID Token中的claims。

第一步,你需要将应用注册到Dex,最简单的方法是使用staticClients配置:

YAML
1
2
3
4
5
6
staticClients:
- id: example-app
  secret: example-app-secret
  name: 'Example App'
  redirectURIs:
  - 'http://127.0.0.1:5555/callback'

在登陆时,应用需要拼接合理的URL,重定向到Dex:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func (a *app) handleLogin(w http.ResponseWriter, r *http.Request) {
    var scopes []string
    if extraScopes := r.FormValue("extra_scopes"); extraScopes != "" {
        scopes = strings.Split(extraScopes, " ")
    }
    var clients []string
    if crossClients := r.FormValue("cross_client"); crossClients != "" {
        clients = strings.Split(crossClients, " ")
    }
    for _, client := range clients {
        scopes = append(scopes, "audience:server:client_id:"+client)
    }
    connectorID := ""
    if id := r.FormValue("connector_id"); id != "" {
        connectorID = id
    }
 
    authCodeURL := ""
    scopes = append(scopes, "openid", "profile", "email")
    if r.FormValue("offline_access") != "yes" {
        authCodeURL = a.oauth2Config(scopes).AuthCodeURL(exampleAppState)
    } else if a.offlineAsScope {
        scopes = append(scopes, "offline_access")
        authCodeURL = a.oauth2Config(scopes).AuthCodeURL(exampleAppState)
    } else {
        authCodeURL = a.oauth2Config(scopes).AuthCodeURL(exampleAppState, oauth2.AccessTypeOffline)
    }
    if connectorID != "" {
        authCodeURL = authCodeURL + "&connector_id=" + connectorID
    }
    //                           身份校验端点
    // http://127.0.0.1:5556/dex/auth?client_id=example-app&redirect_uri=
    // http%3A%2F%2F127.0.0.1%3A5555%2Fcallback&response_type=code&
    // scope=audience%3Aserver%3Aclient_id%3Aexample-app+openid+profile+email+
    // offline_access&state=I+wish+to+wash+my+irish+wristwatch
    http.Redirect(w, r, authCodeURL, http.StatusSeeOther)
}

用户在Dex页面执行操作后, 应用需要处理Dex发回的回调:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
func (a *app) oauth2Config(scopes []string) *oauth2.Config {
    return &oauth2.Config{
        ClientID:     a.clientID,
        ClientSecret: a.clientSecret,
        Endpoint:     a.provider.Endpoint(),
        Scopes:       scopes,
        RedirectURL:  a.redirectURI,
    }
}
 
func (a *app) handleCallback(w http.ResponseWriter, r *http.Request) {
    var (
        err   error
        token *oauth2.Token
    )
 
    ctx := oidc.ClientContext(r.Context(), a.client)
    oauth2Config := a.oauth2Config(nil)
    switch r.Method {
    case http.MethodGet:
        // 这个分支意味着OAuth2授权工作流的回调
        if errMsg := r.FormValue("error"); errMsg != "" {
            http.Error(w, errMsg+": "+r.FormValue("error_description"), http.StatusBadRequest)
            return
        }
        // 授权码在回调的URL参数中传递
        code := r.FormValue("code")
        if code == "" {
            http.Error(w, fmt.Sprintf("no code in request: %q", r.Form), http.StatusBadRequest)
            return
        }
        // 验证state和当初请求登陆时是否一样
        if state := r.FormValue("state"); state != exampleAppState {
            http.Error(w, fmt.Sprintf("expected state %q got %q", exampleAppState, state), http.StatusBadRequest)
            return
        }
        // 刷新Token
        token, err = oauth2Config.Exchange(ctx, code)
    case http.MethodPost:
        // 这个分支意味着用户请求了刷新Token操作
        refresh := r.FormValue("refresh_token")
        if refresh == "" {
            http.Error(w, fmt.Sprintf("no refresh_token in request: %q", r.Form), http.StatusBadRequest)
            return
        }
        t := &oauth2.Token{
            RefreshToken: refresh,
            Expiry:       time.Now().Add(-time.Hour),
        }
        // 使用授权码,再次请求Dex,得到oauth2.Token
        token, err = oauth2Config.TokenSource(ctx, t).Token()
    default:
        http.Error(w, fmt.Sprintf("method not implemented: %s", r.Method), http.StatusBadRequest)
        return
    }
 
    if err != nil {
        http.Error(w, fmt.Sprintf("failed to get token: %v", err), http.StatusInternalServerError)
        return
    }
    // ID Token在OAuth2响应的额外字段中
    rawIDToken, ok := token.Extra("id_token").(string)
    if !ok {
        http.Error(w, "no id_token in token response", http.StatusInternalServerError)
        return
    }
    // 对ID Token进行签名校验
    idToken, err := a.verifier.Verify(r.Context(), rawIDToken)
    if err != nil {
        http.Error(w, fmt.Sprintf("failed to verify ID token: %v", err), http.StatusInternalServerError)
        return
    }
 
    accessToken, ok := token.Extra("access_token").(string)
    if !ok {
        http.Error(w, "no access_token in token response", http.StatusInternalServerError)
        return
    }
 
    // 获取JWT中的claims信息
    var claims json.RawMessage
    if err := idToken.Claims(&claims); err != nil {
        http.Error(w, fmt.Sprintf("error decoding ID token claims: %v", err), http.StatusInternalServerError)
        return
    }
 
    buff := new(bytes.Buffer)
    if err := json.Indent(buff, []byte(claims), "", "  "); err != nil {
        http.Error(w, fmt.Sprintf("error indenting ID token claims: %v", err), http.StatusInternalServerError)
        return
    }
 
    renderToken(w, a.redirectURI, rawIDToken, accessToken, token.RefreshToken, buff.String())
} 
消费ID Token

这类用法中,应用将ID Token作为凭证使用。一个第三方的服务负责处理OAuth流程, 典型的例子是K8S的OpenID Connect Token支持。你可以将ID Token作为K8S API请求的bearer token使用,K8S负责通过数字证书校验此Token的合法性。

消费ID Token,就是去校验它的有效性。你需要创建一个校验器:

Go
1
2
3
4
5
6
provider, err := oidc.NewProvider(ctx, "https://dex-issuer-url.com")
if err != nil {
    // handle error
}
// 构建ID Token校验器,仅仅信任颁发给example-app这个客户端的Token
idTokenVerifier := provider.Verifier(&oidc.Config{ClientID: "example-app"})

然后,校验ID Token,并从中读取可信任的claims: 

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type user struct {
    email  string
    groups []string
}
 
func authorize(ctx context.Context, bearerToken string) (*user, error) {
    idToken, err := idTokenVerifier.Verify(ctx, bearerToken)
    if err != nil {
        return nil, fmt.Errorf("could not verify bearer token: %v", err)
    }
    var claims struct {
        Email    string   `json:"email"`
        Verified bool     `json:"email_verified"`
        Groups   []string `json:"groups"`
    }
    // 校验通过,读取claims
    if err := idToken.Claims(&claims); err != nil {
        return nil, fmt.Errorf("failed to parse claims: %v", err)
    }
    if !claims.Verified {
        return nil, fmt.Errorf("email (%q) in returned claims was not verified", claims.Email)
    }
    return &user{claims.Email, claims.Groups}, nil
}
支持的Scope
Scope 说明
openid 对于所有登陆请求必须
email
email_verified

Dex扩展,非标准

ID Token claims应当包含终端用户的电子邮件信息、电子邮件是否被上游Provider验证过

profile ID Token claims应当包含用户名
groups

Dex扩展,非标准

ID Token claims应当包含用户所属的组

federated:id

Dex扩展,非标准

ID token claims应当包含来自真实IdP的信息,包括connector ID、在IdP中用户的ID

name

Dex扩展,非标准

用户的显示名称

offline_access Token响应中,应该包含一个刷新Token。对于某些Connector是不支持的,例如SAML,会忽略此scope
audience:server:client_id:( client-id ) 动态的Scope,提示Dex,当前验证请求,是代表其它客户端(不是当前客户端)来获取ID Token
跨客户端信任

Dex支持向这样的客户端颁发ID Token:该客户是代表另外一个客户端来请求Token的。

在OIDC的术语中,这种情况意味着ID Token的aud(audience) claim,不是发起请求的那个客户端的client ID。

利用这个特性,一个Web应用可以代表一个命令行工具,来申请ID Token:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
staticClients:
#  Web应用
- id: web-app
  redirectURIs:
  - 'https://web-app.example.com/callback'
  name: 'Web app'
  secret: web-app-secret
# 命令行应用
- id: cli-app
  redirectURIs:
  - 'https://cli-app.example.com/callback'
  name: 'Command line tool'
  secret: cli-app-secret
  # 表示命令行应用信任Web应用,允许后者代表自己去申请ID Token
  trustedPeers:
  - web-app

Web应用发起/auth请求时,可以设置下面的Scope,表示自己在代表命令行应用: audience:server:client_id:cli-app  

Dex返回的ID Token中,会包含如下字段:

JSON
1
2
3
4
5
6
7
{
    // 此Token为哪个客户端(应用)颁发
    "aud": "cli-app",
    // 请求此Token的是谁
    "azp": "web-app",
    "email": "foo@bar.com"
}
公共客户端

可以将客户端标记为公共的:

YAML
1
2
3
4
5
staticClients:
- id: cli-app
  public: true
  name: 'CLI app'
  secret: cli-app-secret

则给应用强加了一个限制:不要尝试将Client Secret作为私有的。

对于公共客户端,它发起请求时指定的回调URL,必须以http://localhost开头,或者指定为一个特殊的out-of-browser地址 urn:ietf:wg:oauth:2.0:oob 。

out-of-browser会导致dex在浏览器中显示OAuth2 code,提示终端用户手工拷贝给其它APP。

存储选项

Dex需要存储一些状态信息,以支持:跟踪刷新Token、防止replay、进行key轮换。 

Ectd
YAML
1
2
3
4
5
6
7
storage:
  type: etcd
  config:
    endpoints:
      - http://localhost:2379
    # 前缀
    namespace: my-etcd-namespace/
K8S CRD

如果在K8S中运行Dex,考虑这种方式。Dex会创建若干类型的CRD,并在CR中存储状态。配置: 

YAML
1
2
3
4
storage:
  type: kubernetes
  config:
    inCluster: true

 

← Kubernetes故障检测和自愈
汪静好作品(一) →

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • 使用JFrog Artifactory管理构件
  • 享学营笔记
  • Eclipse知识集锦
  • 正则表达式
  • IntelliJ平台知识集锦

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • Bazel学习笔记 38 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • NaCl学习笔记 32 people like this
  • PhoneGap学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
  • Three.js学习笔记 24 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2