OIDC搭建之Ory Hydra 2.0实践

背景

我们前期有使用过
Ory Hydra之OAuth 2.0 Authorize Code Flow
Ory Hydra之Oauth 2.0 Client Credentials flow
当时采用的并非2.0,本次完整的使用2.0完整的走一遍,并完整的讲解,如何在授权认证流程对接自己的用户系统。

部署

./docker-compose.yml

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
version: "3.7"
services:
hydra:
image: oryd/hydra:v2.0.2
ports:
- "4444:4444" # Public port
- "4445:4445" # Admin port
- "5555:5555" # Port for hydra token user
command: serve -c /etc/config/hydra/hydra.yml all --dev
volumes:
- type: bind
source: ./config
target: /etc/config/hydra
environment:
- DSN=postgres://hydra:secret@postgresd:5432/hydra?sslmode=disable&max_conns=20&max_idle_conns=4
restart: unless-stopped
depends_on:
- hydra-migrate
networks:
- intranet
hydra-migrate:
image: oryd/hydra:v2.0.2
environment:
- DSN=postgres://hydra:secret@postgresd:5432/hydra?sslmode=disable&max_conns=20&max_idle_conns=4
command: migrate -c /etc/config/hydra/hydra.yml sql -e --yes
volumes:
- type: bind
source: ./config
target: /etc/config/hydra
restart: on-failure
networks:
- intranet
consent:
environment:
- HYDRA_ADMIN_URL=http://hydra:4445
image: oryd/hydra-login-consent-node:v2.0.2
ports:
- "3000:3000"
restart: unless-stopped
networks:
- intranet
postgresd:
image: postgres:11.8
ports:
- "5432:5432"
environment:
- POSTGRES_USER=hydra
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=hydra
networks:
- intranet
networks:
intranet:

./config/hydra.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
serve:
cookies:
same_site_mode: Lax

urls:
self:
issuer: http://127.0.0.1:4444
consent: http://127.0.0.1:3000/consent
login: http://127.0.0.1:3000/login
logout: http://127.0.0.1:3000/logout

secrets:
system:
- youReallyNeedToChangeThis

oidc:
subject_identifiers:
supported_types:
- pairwise
- public
pairwise:
salt: youReallyNeedToChangeThis

演示

Authorization Code Grant && client credentials Grant

创建客户端

2.0开始,不需要client_id,自动生成一个uuid,client_secret不填写,会自动生成。

  • POST请求 http://localhost:4445/admin/clients
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    {
    "client_name": "crm",
    "token_endpoint_auth_method": "client_secret_basic",
    "redirect_uris": [
    "http://127.0.0.1:5555/callback"
    ],
    "scope": "openid offline",
    "grant_types": [
    "authorization_code",
    "refresh_token",
    "implicit",
    "client_credentials"
    ],
    "response_types": [
    "code",
    "id_token",
    "token"
    ]
    }
  • 响应
    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
    {
    "client_id": "a9ea2e4c-5c9e-4edd-8a53-09124b870477",
    "client_name": "crm",
    "client_secret": "A2pnWQdJPYokBG9SvN3zKbnlKL",
    "redirect_uris": [
    "http://127.0.0.1:5555/callback"
    ],
    "grant_types": [
    "authorization_code",
    "refresh_token",
    "implicit",
    "client_credentials"
    ],
    "response_types": [
    "code",
    "id_token",
    "token"
    ],
    "scope": "openid offline",
    "audience": [],
    "owner": "",
    "policy_uri": "",
    "allowed_cors_origins": [],
    "tos_uri": "",
    "client_uri": "",
    "logo_uri": "",
    "contacts": null,
    "client_secret_expires_at": 0,
    "subject_type": "public",
    "jwks": {},
    "token_endpoint_auth_method": "client_secret_basic",
    "userinfo_signed_response_alg": "none",
    "created_at": "2022-11-07T07:14:24Z",
    "updated_at": "2022-11-07T07:14:23.930344Z",
    "metadata": {},
    "registration_access_token": "ory_at_E71s0oXkgZJfLeVn4r7dYsvyanvauuPn6AiQ0uGoh2M.a4MwTXBT6z7rRGVdLK_Cmi-rNF_EH09MymOwpBB6QaE",
    "registration_client_uri": "http://127.0.0.1:4444/oauth2/register/a9ea2e4c-5c9e-4edd-8a53-09124b870477",
    "authorization_code_grant_access_token_lifespan": null,
    "authorization_code_grant_id_token_lifespan": null,
    "authorization_code_grant_refresh_token_lifespan": null,
    "client_credentials_grant_access_token_lifespan": null,
    "implicit_grant_access_token_lifespan": null,
    "implicit_grant_id_token_lifespan": null,
    "jwt_bearer_grant_access_token_lifespan": null,
    "refresh_token_grant_id_token_lifespan": null,
    "refresh_token_grant_access_token_lifespan": null,
    "refresh_token_grant_refresh_token_lifespan": null
    }

请求与响应

普遍的流程为以下三个步骤

  • 1、授权请求 Authorization Request 浏览器打开

    1
    2
    3
    4
    5
    6
    7
    8
    GET {认证终点}
    ?response_type=code // 必选项
    &client_id={客户端的ID} // 必选项
    &redirect_uri={重定向URI} // 可选项
    &scope={申请的权限范围} // 可选项
    &state={任意值} // 推荐
    HTTP/1.1
    HOST: {认证服务器}
  • 2、授权响应 Authorization Response 获取code

    1
    2
    3
    4
    HTTP/1.1 302 Found
    Location: {重定向URI}
    ?code={授权码} // 必填
    &state={任意文字} // 如果授权请求中包含 state的话那就是必填
  • 3、令牌请求 Access Token Request code换token

    1
    2
    3
    4
    5
    6
    7
    8
    POST {令牌终点} HTTP/1.1
    Host: {认证服务器}
    Content-Type: application/x-www-form-urlencoded

    grant_type=authorization_code // 必填
    &code={授权码} // 必填 必须是认证服务器响应给的授权码
    &redirect_uri={重定向URI} // 如果授权请求中包含 redirect_uri 那就是必填
    &code_verifier={验证码} // 如果授权请求中包含 code_challenge 那就是必填

我们按照上面这三个步骤来讲解一下Hydra是怎么做的。

  • 1、Hydra 授权请求 Authorization Request 浏览器打开
    1
    2
    3
    4
    5
    GET http://127.0.0.1:4444/oauth2/auth
    ?response_type=code
    &client_id=a9ea2e4c-5c9e-4edd-8a53-09124b870477
    &scope=openid offline
    &state=nqvresaazswwbofkeztgnvfs
    http://127.0.0.1:4444/oauth2/auth?response_type=code&client_id=a9ea2e4c-5c9e-4edd-8a53-09124b870477&scope=openid offline&state=nqvresaazswwbofkeztgnvfs

打开后我们发现,我们被重定向到了http://127.0.0.1:3000/login?login_challenge=9ba37003126244608ab2d4501f9b32f5

Hydra通过步骤1链接步骤2获取code,抽象为两个流程:Login和Consent,这两个流程便于我们对接我们自己系统的用户授权认证.Login流程主要为 登录认证,Consent流程主要为 授权

我来看看./config/hydra.yml中的配置

1
2
3
consent: http://127.0.0.1:3000/consent   // 授权(前端)
login: http://127.0.0.1:3000/login // 登录认证(前端)
logout: http://127.0.0.1:3000/logout // 登出

Login流程

我们发现重定向的位置就是配置中的login,Login流程是一个登录认证服务(前后端),需要我们在自己业务中实现,链接中还携带了login_challenge,

  • 前端
    此时,我们把账号密码以及login_challenge通过接口发往我们后端
    7Kg0sn

  • 后端
    后端拿到用户名密码和login_challenge,做如下2件事

1、自己业务系统的用户名密码校验
2、携带用户信息和login_challenge调用acceptLoginRequest登录请求

登录请求

请求地址:
http://127.0.0.1:4445/admin/oauth2/auth/requests/login/accept?login_challenge=66cc8259bf0c4a3880e26c189968bbd6
请求方式:PUT
请求类型:application/json
请求参数:

1
2
3
4
5
6
7
8
{
"subject": "foo@bar.com",
"acr": "1",
"context": {},
"force_subject_identifier": "2",
"remember": false,
"remember_for": -4068005
}

请求成功返回:

1
2
3
{
"redirect_to": "http://127.0.0.1:4444/oauth2/auth?client_id=624b45d4-ef0f-4bec-a6be-9c18e7103c3e&login_verifier=b458cfc4152a4d9389fc52413087c020&response_type=code&scope=openid+offline&state=nqvresaazswwbofkeztgnvfs"
}

打开这个重定向,就进入了Consent流程

Consent流程

  • 前端
    此时,我们把用户授权以及consent_challenge通过接口发往我们后端
    miv0HD

  • 后端
    后端拿到用户授权以及consent_challenge,做如下2件事
    1、自己业务系统的授权
    2、consent_challenge调用acceptLoginRequest认证请求

认证请求

请求地址:http://127.0.0.1:4445/admin/oauth2/auth/requests/consent/accept?consent_challenge=xxxxxx
请求方式:PUT
请求类型:application/json
请求参数:

说明下,session是可以写你想要放进id_token里面的东西,但是但是!请不要有中文,比如说:”name”:”小白”,这样Hydra也无法识别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"grant_access_token_audience": [],
"grant_scope": [
"openid",
"offline",
],
"handled_at": "2019-04-16T04:45:05.685Z",
"remember": false,
"remember_for": -72766940,
"session": {
"access_token": {},
"id_token": {
"userId": "111"
}
}
}

请求成功返回:

1
2
3
{
"redirect_to": "http://127.0.0.1:4444/oauth2/auth?client_id=624b45d4-ef0f-4bec-a6be-9c18e7103c3e&consent_verifier=2f77bd26c6504ccb8a5e88d65a5818b7&response_type=code&scope=openid+offline&state=nqvresaazswwbofkeztgnvfs"
}

重定向打开后,完成Consent流程,获取到code,此时会将code携带到我们创建应用时候的redirect_uris=http://127.0.0.1:5555/callback

1
http://127.0.0.1:5555/callback?code=ory_ac_0T0UehFyo-BVCDcdiu2qUuxLw4jNLpwFDjqkC157-ms.eUZpm0ZokBUBdxgEI5y5w8BTjf1URAzMwwXddW3gf4Q&scope=openid+offline&state=nqvresaazswwbofkeztgnvfs

token获取

获取令牌、刷新令牌

请求地址:
http://127.0.0.1:5444/oauth2/token
请求方式:POST
请求类型:application/x-www-form-urlencoded

请求参数 参数类型 参数说明
grant_type 字符串 授予类型,必填项
code 字符串 授权码
refresh_token 字符串 刷新令牌
client_id 字符串 客户端id,必填项
client-secret 字符串 客户端秘钥,必填项
redirect_uri 字符串 重定向uri

Authorization使用 Basic Auth 将client_id和client_secret写入

1
2
3
4
5
6
{
"grant_type": "authorization_code",
"client_id": "facebook-photo-backup",
"redirect_uri": "http://localhost:9020/login"
"code": "Qk4jf3dZ_DSkAAtlbS9pTilVFTRCeAYHdPpUN"
}

nN5AYX

Yt3jVA

返回

1
2
3
4
5
6
7
8
{
"access_token": "ory_at_D0wqbtwGY_rtFdy_wEfqhyQGmD7V358y8XWw_94AvGM.cEbzrl27tVxIQO6fJMDBJgCO72OAenBuZgXv1VPUrDc",
"expires_in": 3600,
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjhmYjRlMjlhLTZlZmItNGIxMy04ODM2LTM5M2ZjM2I1NWUyOSIsInR5cCI6IkpXVCJ9.eyJhY3IiOiJsYWJvIiwiYXRfaGFzaCI6InRiMjcza0kwWWhkYk52by0zQ0FKYmciLCJhdWQiOlsiYTllYTJlNGMtNWM5ZS00ZWRkLThhNTMtMDkxMjRiODcwNDc3Il0sImF1dGhfdGltZSI6MTY2NzgwODIxNSwiZXhwIjoxNjY3ODExODYyLCJpYXQiOjE2Njc4MDgyNjIsImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6NDQ0NCIsImp0aSI6ImNlN2UwNGI4LTI1ODUtNGQzYi1hMjBhLTA1OThmMDdjZDUyOCIsInJhdCI6MTY2NzgwODIwMiwic2lkIjoiMDE4ZDc3MWEtMjQyNi00YzhhLWFmZjQtYjQ3MGJlYWM5NGI0Iiwic3ViIjoiZm9vQGJhci5jb20iLCJ1c2VySWQiOiIxMTEifQ.FuUDY0w94H9SPFr8iakHvEo63w9RTVqjHgjzi7gngHgL6sRV3yP9-hZrc4HBZys_PFT5KP_bQra3IKqM-OhF9UZZnfXM4je6HSAW8XdX0PbMZQGut1_5jh8rZjqXPJNY_YL2CNnm4YhID7CO-sEIqcBrVu1O30l44cC93NJJbU9N8wrlHf4H2ROoUhkpPl8WSoRDviUX0NB6dg3Y87q8MDLUTjvQpLNK7SejSI9c6AzNyQneGYBVAVksxItluulWcLgjM98gmZ_35jge5KeOel8q0kpdjbKIOfDCva8PibXoSWZtIvCi4EHYE2aSvu5TL1NlaDhkzE-tuuxjmQJJdIeLOy-kcDFd63t-l3k9dy859UM7B6BNKKFcHmc5bkg2BRf7iZxc7Q6BEvi2F7mrsThJYFtpTNjQCCOsO-E3d2WXi7uFwSI_qQpE5eAcBa0-qivv8RHUqiFIhNDNp1WYk2yDCgqeQx3NokZ03N4oM_CWCnyt2M0WKnofPL0YpnZXiIzxM_KvnqTfZy9ckoVj7gf1H9yZkhQunQVx2oIFIcEqshA1cbvtJ-XN5mZgLAnwSYN3_vRUsW3GQIP4GCT8zf8CVIW-7H5JkWSSLs2DrnexwYEuMX-6TttytflF1FNru4TfF539z9HkBp35-aa8xvh1j-GFSXwlaUr2KKjeTDU",
"refresh_token": "ory_rt_PNaBzXflt2ICDwbh7j68eerGO-8HEtC8S6WzUlTNknQ.6ih8eHalnW5zNwHM2RQktv1WDjSOw3S7mwzUutMMLk8",
"scope": "openid offline",
"token_type": "bearer"
}

最后我们拿着idToken去JWT解析看看效果可以看到,idToken解析出来你需要的信息。
至此我们就获取到了访问令牌access_token,前端可以将令牌缓存在cookie或session中,相应的后台也会缓存,后面前端调用其他服务时携带令牌调用接口,后台校验根据token来判断是否放行。

vXRiND

相关

5min-tutorial
jq

cli相关操作

cli 创建客户端

1
2
3
4
5
6
7
8
9
10
11
code_client=$(docker-compose -f docker-compose.yml exec hydra \
hydra create client \
--endpoint http://127.0.0.1:4445 \
--grant-type authorization_code,refresh_token \
--response-type code,id_token \
--format json \
--scope openid --scope offline \
--redirect-uri http://127.0.0.1:5555/callback)

code_client_id=$(echo $code_client | jq -r '.client_id')
code_client_secret=$(echo $code_client | jq -r '.client_secret')

使用hydra示例授权(Hydra 提供快速验证oauth授权流程)

1
2
3
4
5
6
7
docker-compose -f docker-compose.yml exec hydra \
hydra perform authorization-code \
--client-id $code_client_id \
--client-secret $code_client_secret \
--endpoint http://127.0.0.1:4444/ \
--port 5555 \
--scope openid --scope offline