Ory Hydra之OAuth 2.0 Authorize Code Flow

背景

单独把身份认证服务部署起来,在调用其他服务的时候,先通过身份认证。

因为Hydra版本出现了大的变动,即将2.0,改动巨大,本篇暂时以v1.10.6进行

本次我们来走一遍 OAuth 2.0 Authorize Code Flow

部署

启动Hydra服务

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
# 创建一个独立的网段
docker network create hydraguide

# 拉取pg,当然也可使用mysql等其他数据库
docker pull postgres:9.6

# 拉取hydra
docker pull oryd/hydra:v1.10.6

# 运行数据库(帐号:hydra 密码:secret)
docker run \
--network hydraguide \
--name ory-hydra-example--postgres \
-e POSTGRES_USER=hydra \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=hydra \
-d postgres:9.6

# 设置加密
export SECRETS_SYSTEM=$(export LC_CTYPE=C; cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
# 当然你也可以写死
export SECRETS_SYSTEM=SHARINGTOMMY123456789

# 创建临时的环境变量 DSN
export DSN=postgres://hydra:secret@ory-hydra-example--postgres:5432/hydra?sslmode=disable

# 初始化数据库
docker run -it --rm \
--network hydraguide \
oryd/hydra:v1.10.6 \
migrate sql --yes $DSN

# 启动一个Hydra服务
docker run -d \
--name ory-hydra-example--hydra \
--network hydraguide \
-p 5444:4444 \
-p 5445:4445 \
-e SECRETS_SYSTEM=$SECRETS_SYSTEM \
-e DSN=$DSN \
-e URLS_SELF_ISSUER=http://localhost:5444/ \
-e URLS_CONSENT=http://localhost:9020/consent \
-e URLS_LOGIN=http://localhost:9020/login \
oryd/hydra:v1.10.6 serve all --dangerous-force-http

# 验证(能看到正常启动日志)
docker logs ory-hydra-example--hydra

说明:
--network hydraguide 网络
-p 5444:4444 public API http://localhost:5444/
-p 5445:4445 Hydra’s administrative API http://localhost:5445/
-e SECRETS_SYSTEM=$SECRETS_SYSTEM 加密变量
-e DSN=$DSN 数据库变量
-e URLS_SELF_ISSUER=http://localhost:5444/ 是你的服务器地址
-e URLS_CONSENT=http://localhost:9020/consent 是你前端用户同意授权地址
-e URLS_LOGIN=http://localhost:9020/login 是前端用户登录地址
-e URLS_LOGOUT 是你退出登录地址
-e URLS_POST_LOGOUT_REDIRECT 是你退出登录成功后跳转到的地址
-e TTL_ID_TOKEN id_token 过期时间的设置单位 h m s,默认为1小时
-e TTL_ACCESS_TOKEN 配置刷新令牌有效的时间。默认值为720h。设置为-1可使刷新令牌永不过期。
-e TTL_REFRESH_TOKEN配置标识令牌有效的时间。默认为1小时。

–dangerous-force-http 加了这句话就是不需要 https
如果你不加的话,URLS_SELF_ISSUER=https://localhost:4444/ 这里就要加s
加了https,https会有证书等问题。

登录/授权样例网站启动

该部分一般就是我们的前端登录授权页面,只是hydra提供了一个示例

1
2
3
4
5
6
7
8
docker pull oryd/hydra-login-consent-node:v1.10.6
docker run -d \
--name ory-hydra-example--consent \
-p 9020:3000 \
--network hydraguide \
-e HYDRA_ADMIN_URL=http://ory-hydra-example--hydra:4445 \
-e NODE_TLS_REJECT_UNAUTHORIZED=0 \
oryd/hydra-login-consent-node:v1.10.6

-p 9020:3000暴露9020端口,这个端口就是URLS_CONSENTURLS_LOGIN(URLS_CONSENT=http://localhost:9020/consent, URLS_LOGIN=http://localhost:9020/login).
HYDRA_ADMIN_URL=http://ory-hydra-example--hydra:4445 Hydra后台管理接口
NODE_TLS_REJECT_UNAUTHORIZED=0 取消TLS校验

示例页面:
fGU37K

OexXJh

演示OAuth2.0流程

图解

3YxYQv
xCH2Wd
TvxaSN
iFMa3c
CKoZI4
v6NTz1
icA7m3
0N432n
ITxNk0
qJn2BQ
GAmN24
8P0SbL

请求与响应

  • 授权请求 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: {认证服务器}
  • 授权响应 Authorization Response 获取code

    1
    2
    3
    4
    HTTP/1.1 302 Found
    Location: {重定向URI}
    ?code={授权码} // 必填
    &state={任意文字} // 如果授权请求中包含 state的话那就是必填
  • 令牌请求 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 那就是必填

根据具体情况有可能是向客户端服务器进行请求,这时候请加上 Basic 认证(Authorization 头部)或者是 参数 client_id & client_secret

  • 令牌响应 Access Token Response
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    HTTP/1.1 200 OK
    Content-Type: application/json;charset=UTF-8
    Cache-Control: no-store
    Pragma: no-cache

    {
    "access_token":"{访问令牌}", // 必填
    "token_type":"{令牌类型}", // 必填
    "expires_in":{过期时间}, // 任意
    "refresh_token":"{刷新令牌}", // 任意
    "scope":"{授权范围}" // 如果请求和响应的授权范围不一致就必填
    }

Hydra演示

创建一个facebook-photo-backup应用并获得id和secret

通过 Hydra CLI 命令创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
docker run --rm -it \
-e HYDRA_ADMIN_URL=http://ory-hydra-example--hydra:4445 \
--network hydraguide \
oryd/hydra:v1.10.6 \
clients create --skip-tls-verify \
--id facebook-photo-backup \
--secret some-secret \
--grant-types authorization_code,refresh_token,client_credentials,implicit \
--response-types token,code,id_token \
--scope openid,offline,photos.read \
--callbacks http://127.0.0.1:9010/callback


You should not provide secrets using command line flags, the secret might leak to bash history and similar systems
OAuth 2.0 Client ID: facebook-photo-backup

测试环境会有提示You should not provide secrets using command line flags, the secret might leak to bash history and similar systems,忽略即可。
此时我们得到

1
2
Client ID: facebook-photo-backup
Client Secret: some-secret
通过 rest api 创建

创建应用客户端
P3Hu1k

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST http://localhost:5445/clients
{
"client_id": "facebook-photo-backup1",
"client_secret": "some-secret",
"token_endpoint_auth_method": "client_secret_basic",
"redirect_uris": [
"http://127.0.0.1:9010/callback"
],
"scope": "openid offline photos.read",
"grant_types": [
"authorization_code",
"refresh_token",
"implicit",
"client_credentials"
],
"response_types": [
"code",
"id_token",
"token"
]
}

执行一个 OAuth 2.0 授权流程

使用CLI简易

以下示例将执行一个 OAuth 2.0 授权流程。为简化此操作,Hydra CLI 提供了一个名为 hydra token user 的辅助命令。

1
2
3
4
5
6
7
8
9
10
11
docker run --rm -it \
--network hydraguide \
-p 9010:9010 \
oryd/hydra:v1.10.6 \
token user --skip-tls-verify \
--port 9010 \
--auth-url http://localhost:5444/oauth2/auth \
--token-url http://ory-hydra-example--hydra:4444/oauth2/token \
--client-id facebook-photo-backup \
--client-secret some-secret \
--scope openid,offline,photos.read

上面这个服务的过程请参考 下面的 使用Hydra api,其实就是简化接口参数过程。

打开 http://127.0.0.1:9010/

ggvyYz

fGU37K

OexXJh

SCz661

这个时候我们使用Access Token去调用userinfo API,即可正常获取到用户信息.

1
2
3
4
5
6
7
curl -X GET \
http://localhost:5444/userinfo \
-H 'authorization: Bearer 6BboGKvjsyXG_ZFqX8NQboVi4v4JFqiLoKRZ2ex3QRI.fowTdOf3QqCojmmKTdm5MveKulsv-vcEkp2KdiROAMI' \
-H 'cache-control: no-cache' \
-H 'content-type: application/json' \
-H 'postman-token: fecb7032-db0a-bb1b-a61b-de22add7e5bc'

得到如下信息,sub就是用户的信息,为什么这里用户信息只有一个sub呢?因为他们实现ory-hydra-example–consent的时候什么都没加进去,根据自己需要的信息加入sub就可以了。

1
2
3
4
5
6
7
8
{
"subject": "foo@bar.com",
"acr": "labo",
"context": "<object>",
"force_subject_identifier": "ex fugiat aliquip amet dolore",
"remember": false,
"remember_for": -4068005
}
使用Hydra api

YljCLk
HTSmvp

整个过程最终目的就是要获取授权码,然后通过授权码去拿token,通过图可以看出获取授权码一共需要两个流程:LoginConsent

下面我们就开始完整的演示一遍获取授权码的接口流程:

浏览器打开:
http://localhost:5444/oauth2/auth?&client_id=facebook-photo-backup&response_type=code&scope=openid&state=nqvresaazswwbofkeztgnvfs

Hydra服务器会302重定向到你在创建的时候设定的前端登录地址:http://localhost:9020/login?login_challenge=xxxxxxxxxx,带着login_challenge回来,这个东西就是下面接口需要的东西

7Kg0sn
这时候,我们前端应该进行身份验证的提交,并携带login_challenge到后端服务,后端接收到,通过对用户名和密码的校验后,请求acceptLoginRequest

登录请求

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

1
2
3
4
5
6
7
8
{
"subject": "foo@bar.com",
"acr": "labo",
"context": "<object>",
"force_subject_identifier": "ex fugiat aliquip amet dolore",
"remember": false,
"remember_for": -4068005
}

请求成功返回:

1
2
3
{
"redirect_to": "http://localhost:5444/oauth2/auth?client_id=facebook-photo-backup&login_verifier=289c0dbac2eb49e28eebc4b8f208b8c8&response_type=code&scope=openid&state=nqvresaazswwbofkeztgnvfs"
}

lwBuzu

MpbDZl

我们再通过浏览器打开

Login认证流程就结束了,把请求成功返回的结果,继续丢到浏览器中,他会302重定向到你创建hydra的时候设定的consent地址,并携带consent_challenge,,我们再通过前端提交把consent_challenge 的值传递给后端,让后端拿到下面去继续请求

认证请求

请求地址:
http://localhost:5445/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
{
"grant_access_token_audience": [],
"grant_scope": [
"openid"
],
"handled_at": "2019-04-16T04:45:05.685Z",
"remember": true,
"remember_for": -72766940,
"session": {
"access_token": {},
"id_token": {
"userId": "111"
}
}
}

请求成功返回:

1
2
3
{
"redirect_to": "http://localhost:4444/oauth2/auth?client_id=tommy&consent_verifier=373e5b86d4444fe2a78390df64efc9b1&prompt=&response_type=code&scope=openid&state=nqvresaazswwbofkeztgnvfs"
}

z5hXVM
把结果的验证接口地址继续放到浏览器中回车,hydra服务器会重定向到你创建应用时候设置的callback地址,并且后面带着code,如:http://127.0.0.1:9010/callback?code=xxxxxxxxx,拿到这个code到下面的接口,就可以请求获取到Token了。

Jd7Vsk

获取令牌、刷新令牌

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

请求参数 参数类型 参数说明
grant_type 字符串 授予类型,必填项
code 字符串 授权码
refresh_token 字符串 刷新令牌
client_id 字符串 客户端id,必填项
client-secret 字符串 客户端秘钥,必填项
redirect_uri 字符串 重定向uri
1
2
3
4
5
6
{
"grant_type": "authorization_code",
"client_id": "facebook-photo-backup",
"redirect_uri": "http://localhost:9020/login"
"code": "Qk4jf3dZ_DSkAAtlbS9pTilVFTRCeAYHdPpUN"
}

返回

1
2
3
4
5
6
7
8
{
"access_token": "nRNAO1g8LNMI2i_FJXQDoLxvHL7aLz4sILhWoL_de4w.EcGjSbAlCmsBSPlE_KtNk99AcFVaE_eqye0Sh41dXSk",
"expires_in": 299,
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpjNDk2N2ZkYi0yYTg0LTRhNjAtODRlZi02M2Q3MGFmNTRmNzIifQ.eyJhdF9oYXNoIjoia05wVkVkWGhTa2U4MmdvSk81SUFqUSIsImF1ZCI6WyJ6c3ctY2xvdWQtZGV2Il0sImF1dGhfdGltZSI6MTY1ODYyOTIxOSwiZXhwIjoxNjU4NjI5NTI2LCJpYXQiOjE2NTg2MjkyMjYsImlzcyI6Imh0dHBzOi8venN3LWh5ZHJhLmNvbS8iLCJqdGkiOiI1ZmIwMWM4NC1mNTZmLTQ0MGUtYjFmZC1iOTBjODNhY2ZkODIiLCJyYXQiOjE2NTg2MjkyMTIsInNpZCI6IjU2OWY2ZDczLTgzZDEtNDBlOS04YmMxLWFlMjFhY2QzOGYyZCIsInN1YiI6InpzdyJ9.OaAlvwFY84BU2_fF8RxsXK_ueoURmvIMl_Xa7xZ566laeZdJ8GyONzrlGDSLwNNhdKV8Mcl3U8aNoGZDb5w3DRca9C0rqaedo-r4zMrsAZ-YNUAXvuv_Ga-n_MDPA2FxLF0vz1Til48jkbWhQ0QmJnT_m6DvUo4veVjtbU6Ggbz2-rYO7adW2rp1gf4I_AwwUOjfBtQmqZRPNvQIkX-Md-bQfhqnGikMEkeoZdYuZP3ags6H1cm3E8eMLyJk4kGXGkMosSKLE8LFh1HrXYQfCDwCVpL1dy_-b0ZKyj20RVVdusBzdb97MV4QFeKleuyGIRBXHI0etW9EELOVjPWcz59tuE29uToSopiEArFpeCotsh4nllFxqtvqRM4zh5ZMjf6MIHpm74IW8nVlXdCVjBjzZp3Lg3th7iWEDrZm_9tZ1o0SmYYwf9IbjjttrIaBbph-iTm5aijN6WHrKM0HNOcrERrcK4REcSFKueL46-yHRKmOhwXNROJHZQu3mTpZRO8BnR3eWBsRuFmVGLt8BKi8s_fAR7AI__WN1y8rek2_34LnAVrh8CJQnzBAIB-9y6AeGH8a9t_tqxkJWeLa8ohXVH8VTceKkCMNm_7x9vvhhqlb8lyVau9ktvkIgoalyGRmBf66FZQkxpDFht0XiC7ZGq9IusI-fDSIcGRuJa8",
"refresh_token": "emWnXUsZ43Sb9_eR0HyxirTltitFX0rvv2ouwRHdZ6U.4zKlg3iCdoHco02jryMHj432xzz0yQrh41zaQejp52M",
"scope": "openid offline",
"token_type": "bearer"
}

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

以上就是获取token的整个流程。

当然token也有过期的时候,下面说一下刷新令牌,接口和上面获取token是同一个http://localhost:5444/oauth2/token,只是传参不同

1
2
3
4
5
6
7
{
"grant_type": "refresh_token",
"client_id": "facebook-photo-backup",
"redirect_uri": "http://localhost:9020/login"
// refresh_token是获取token时的refresh_token,不是access_token
"refresh_token": "emWnXUsZ43Sb9_eR0HyxirTltitFX0rvv2ouwRHdZ6U.4zKlg3iCdoHco02jryMHj432xzz0yQrh41zaQejp52M"
}

可用于用户登出

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

1
2
3
{
"token":""
}

可用于获取用户标识或校验token是否存活

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

1
2
3
{
"token":""
}

相关链接

Run Ory Hydra in Docker
deprecate –dangerous-force-http flag
[简易图解]『 OAuth2.0』 猴子都能懂的图解
[简易图解]『 OAuth2.0』 『进阶』 授权模式总结
微服务Token方案之ORY Hydra授权中心_Java实现