在之前的文章中已经介绍了Kong这个api网关的安装基本打开方式。这篇文章介绍一下kong在某个RouteService中使用OAuth2.0的认证插件进行OAuth2的认证。

环境准备

创建Service

创建一个Kong的Service Object指向上游的服务。我会使用httpbin作为上游服务作为演示。

REQUEST:

1
2
3
4
curl -X POST \
--url "http://localhost:8001/services" \
--data "name=oauth2-test" \
--data "url=http://cakepanit.org/anything"

预期RESPONSE:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"host": "httpbin.org",
"id": "33459a79-e284-4bb8-aa6f-65dafd456c6f",
"protocol": "http",
"read_timeout": 60000,
"tls_verify_depth": null,
"port": 80,
"updated_at": 1615001132,
"ca_certificates": null,
"created_at": 1615001132,
"connect_timeout": 60000,
"write_timeout": 60000,
"name": "oauth2-test",
"retries": 5,
"path": "/anything",
"tls_verify": null,
"tags": null,
"client_certificate": null
}

创建Route

接下来我会创建路径/demo来访问服务。
REQUEST:

1
2
3
curl -X POST \
--url "http://localhost:8001/services/oauth2-test/routes" \
--data 'paths[]=/demo'

预期RESPONSE:

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
{
"strip_path": true,
"tags": null,
"updated_at": 1615004204,
"destinations": null,
"headers": null,
"protocols": [
"http",
"https"
],
"methods": null,
"service": {
"id": "33459a79-e284-4bb8-aa6f-65dafd456c6f"
},
"snis": null,
"hosts": null,
"name": null,
"path_handling": "v0",
"paths": [
"/demo"
],
"preserve_host": false,
"regex_priority": 0,
"response_buffering": true,
"sources": null,
"id": "e804fef4-fa42-4f7e-be0c-bbe9b9999027",
"https_redirect_status_code": 426,
"request_buffering": true,
"created_at": 1615004204
}

在这之后我们可以通过 curl localhost:8000/demo来访问上游服务。

启用OAuth2插件

我会在我们的service object上启用这个插件并且自定义 provision_key . 如果你不自定义这个变量的话,kong会自动生成一个。同时我这里启用了全部四种认证方式以做演示,在现实中你只需要启用你需要的grant。

REQUEST:

1
2
3
4
5
6
7
8
9
10
11
12
curl -X POST \
--url http://localhost:8001/services/oauth2-test/plugins/ \
--data "name=oauth2" \
--data "config.scopes[]=email" \
--data "config.scopes[]=phone" \
--data "config.scopes[]=address" \
--data "config.mandatory_scope=true" \
--data "config.provision_key=oauth2-demo-provision-key" \
--data "config.enable_authorization_code=true" \
--data "config.enable_client_credentials=true" \
--data "config.enable_implicit_grant=true" \
--data "config.enable_password_grant=true"

预期RESPONSE:

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
{
"created_at": 1615003048,
"id": "8bc8ed59-cdb4-4ac4-ab48-7719655cb9f3",
"tags": null,
"enabled": true,
"protocols": [
"grpc",
"grpcs",
"http",
"https"
],
"name": "oauth2",
"consumer": null,
"service": {
"id": "33459a79-e284-4bb8-aa6f-65dafd456c6f"
},
"route": null,
"config": {
"pkce": "lax",
"accept_http_if_already_terminated": false,
"reuse_refresh_token": false,
"token_expiration": 7200,
"mandatory_scope": true,
"enable_client_credentials": true,
"hide_credentials": false,
"enable_authorization_code": true,
"enable_implicit_grant": true,
"global_credentials": false,
"refresh_token_ttl": 1209600,
"enable_password_grant": true,
"scopes": [
"email",
"phone",
"address"
],
"anonymous": null,
"provision_key": "oauth2-demo-provision-key",
"auth_header_name": "authorization"
}
}

此时我们再次链接 curl localhost:8000/demo 的时候,我们会得到 HTTP/1.1 401 Unauthorized已经以下错误信息。这就说明Oauth2插件已经成功开启。

1
2
3
4
{
"error": "invalid_request",
"error_description": "The access token is missing"
}

创建Consumer

接下来我们需要创建consumer objectd
REQUEST:

1
2
3
curl -X POST \
--url "http://localhost:8001/consumers/" \
--data "username=oauth2-tester"

RESPONSE:

1
2
3
4
5
6
7
{
"custom_id": null,
"created_at": 1615003502,
"id": "06d53376-8bfd-4bc7-aaaf-05c37316e7ef",
"tags": null,
"username": "oauth2-tester"
}

创建OAuth2 Credential (App)

然后我们会在这个consumer下创建Oauth2的身份凭证。在这里我也会使用自定义的client_idclient_secret。如果留空,Kong会自动生成这两个变量。

如果使用Kong来生成身份凭证请切记不要添加 hash_secret=true 在您的curl命令里面。

REQUEST:

1
2
3
4
5
6
7
curl -X POST \
--url "http://localhost:8001/consumers/oauth2-tester/oauth2/" \
--data "name=Oauth2 Demo App" \
--data "client_id=oauth2-demo-client-id" \
--data "client_secret=oauth2-demo-client-secret" \
--data "redirect_uris[]=http://localhost:8000/demo" \
--data "hash_secret=true"

RESPONSE:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"created_at": 1615004674,
"id": "f602f09b-7b7e-4326-b236-2fa8d45badff",
"tags": null,
"name": "Oauth2 Demo App",
"client_secret": "$pbkdf2-sha512$i=10000,l=32$e3SNVIWRFt8PuBxjoL1ncQ$/hF26HS30QHopDLMzlZqC+zv0nt3m4YFokuW9eTma6Q",
"client_id": "oauth2-demo-client-id",
"redirect_uris": [
"http://localhost:8000/demo"
],
"hash_secret": true,
"client_type": "confidential",
"consumer": {
"id": "06d53376-8bfd-4bc7-aaaf-05c37316e7ef"
}
}

接下来我们就可以测试不同的grant了。

OAuth的四种授权类型

参见: OAuth Grant Types,根据业务场景选择一种合适的类型,这里不再赘述。

Authorization Code

我们需要先发送认证的请求到https://localhost:8443/demo/oauth2/authorize 获取一个授权码。然后再使用授权码到https://localhost:8443/demo/oauth2/token请求一个访问令牌。

Request Authorized Code

REQUEST:

1
2
3
4
5
6
7
8
curl -X POST \
--url "https://localhost:8443/demo/oauth2/authorize" \
--data "response_type=code" \
--data "scope=email address" \
--data "client_id=oauth2-demo-client-id" \
--data "provision_key=oauth2-demo-provision-key" \
--data "authenticated_userid=authenticated_tester" \
--insecure

RESPONSE:

1
2
3
{
"redirect_uri": "http://localhost:8000/demo?code=jvnD1XBgFqZuqT2OlbcXpDiOlFkx75bU"
}

这样我们就获取到我们的授权码 jvnD1XBgFqZuqT2OlbcXpDiOlFkx75bU,我们可以通过它来请求访问令牌。

Request Access Token

REQUEST:

1
2
3
4
5
6
7
curl -X POST \
--url "https://localhost:8443/demo/oauth2/token" \
--data "grant_type=authorization_code" \
--data "client_id=oauth2-demo-client-id" \
--data "client_secret=oauth2-demo-client-secret" \
--data "code=oyPC89DOjHNNc7BBV9YWuUxJQDd5M1TU" \
--insecure

RESPONSE:

1
2
3
4
5
6
{
"refresh_token": "LqJW6mVH4XsNZnoQ5fYbjngBsbUJPVPh",
"token_type": "bearer",
"access_token": "BZiZzJVEuP2mgNvZZBr0mgbRtKsdqgZf",
"expires_in": 7200
}

Implicit

因为安全性的问题,这个可能是你不应该使用的一个grant。更多的原因可以参考Okta的这篇文章。按照我的理解Implicit grant应该只用来做身份的认证而不应该返回通行码用来使用API。使用这个grant的话只需要发送client_id到认证服务器https://localhost:8443/demo/oauth2/authorize就能获取到通行码。

Request Access Token

REQUEST:

1
2
3
4
5
6
7
8
curl -X POST \
--url "https://localhost:8443/demo/oauth2/authorize" \
--data "response_type=token" \
--data "scope=email address" \
--data "client_id=oauth2-demo-client-id" \
--data "provision_key=oauth2-demo-provision-key" \
--data "authenticated_userid=authenticated_tester" \
--insecure

RESPONSE:

1
2
3
{
"redirect_uri": "http://localhost:8000/demo#access_token=Ubs61rbN0JSO6JMV9N7WC4ZvCWEpWp5z&expires_in=7200&token_type=bearer"
}

你可以在返回的uri的fragment里面找到access_token

Client Credentials

这个grant主要应用在机器对机器之间,因此认证服务器只会返回通行码而不会返回刷新码。每次通行码过期之后都需要重新请求新的。

Request Access Code

REQUEST:

1
2
3
4
5
6
7
8
curl -X POST \
--url "https://localhost:8443/demo/oauth2/token" \
--data "grant_type=client_credentials" \
--data "scope=email address" \
--data "client_id=oauth2-demo-client-id" \
--data "client_secret=oauth2-demo-client-secret" \
--data "provision_key=oauth2-demo-provision-key" \
--insecure

RESPONSE:

1
2
3
4
5
{
"token_type": "bearer",
"access_token": "7mKwrytPCZEgTjbr40rV5L0dk2Zykota",
"expires_in": 7200
}

Password

该flow需要用户提供认证的用户名,因此用户需要在Kong的前面添加用户身份认证并且提供authenticated_userid给Kong来颁发通行码和刷新码给用户。

Request Access Code

REQUEST:

1
2
3
4
5
6
7
8
9
curl -X POST \
--url "https://localhost:8443/demo/oauth2/token" \
--data "grant_type=password" \
--data "scope=email address" \
--data "client_id=oauth2-demo-client-id" \
--data "client_secret=oauth2-demo-client-secret" \
--data "provision_key=oauth2-demo-provision-key" \
--data "authenticated_userid=authenticated_tester" \
--insecure

RESPONSE:

1
2
3
4
5
6
{
"refresh_token": "rYokjg6H8Vi23xcdLKJhGBbt4OkbNTpy",
"token_type": "bearer",
"access_token": "2TVKDlWyoFODlNuIvQaLdCFU7Ids8Gyk",
"expires_in": 7200
}

使用 access_token

1
2
3
curl -X GET \
--url "http://localhost:8000/demo" \
--header "Authorization: Bearer <ACCESS_TOKEN>"

使用PKCE

在使用authorization code grant的时候,用户可以使用PKCE加强安全性。

生成Verifier and Challenge

现实中用户需要自己想办法在自己的程序中按需自动生成这两个变量。在我们的演示中,我会使用https://tonyxu-io.github.io/pkce-generator/来生成。

1
2
3
4
5
Code Verifier:
8FK~B.3ERQPsn4xoSo.7pkmxc6wEiFabpqooHnFJKyyT3ZI41jh9DML0TA7UTVTYrxhUtsNfcOp9RcVhyKR~2GdWCFlv00WKFJ1ha_acuzeuyFYDI1.j4nJ3epQUmc0w

Code Challenge:
nVFqpBvGXtATi0hhNnNuWE5PZNRQTNGR95DJZNcXEaU

Request Authorized Code

REQUEST:

1
2
3
4
5
6
7
8
9
10
curl -X POST \
--url "https://localhost:8443/demo/oauth2/authorize" \
--data "response_type=code" \
--data "scope=email address" \
--data "client_id=oauth2-demo-client-id" \
--data "provision_key=oauth2-demo-provision-key" \
--data "authenticated_userid=authenticated_tester" \
--data "code_challenge=nVFqpBvGXtATi0hhNnNuWE5PZNRQTNGR95DJZNcXEaU" \
--data "code_challenge_method=S256" \
--insecure

RESPONSE:

1
2
3
{
"redirect_uri": "http://localhost:8000/demo?code=5CzRTGquWIq7ePVjuX0Yyx5XZsCZUlML"
}

Request Access Token

REQUEST:

1
2
3
4
5
6
7
8
curl -X POST \
--url "https://localhost:8443/demo/oauth2/token" \
--data "grant_type=authorization_code" \
--data "client_id=oauth2-demo-client-id" \
--data "client_secret=oauth2-demo-client-secret" \
--data "code_verifier=8FK~B.3ERQPsn4xoSo.7pkmxc6wEiFabpqooHnFJKyyT3ZI41jh9DML0TA7UTVTYrxhUtsNfcOp9RcVhyKR~2GdWCFlv00WKFJ1ha_acuzeuyFYDI1.j4nJ3epQUmc0w" \
--data "code=51yMXT93QTQUn0zqbvBsUsWNNRRT3sce" \
--insecure

RESPONSE:

1
2
3
4
5
6
{
"refresh_token": "c3blcIo9QwexfYAGne98u91vKBd3W9pW",
"token_type": "bearer",
"access_token": "pCMgPx97ApLJFM9zIv6TmdXEWmH5xlLq",
"expires_in": 7200
}

Refresh Token

在上述的例子中,各位可以看到authorization_codepasswordgrant会返回一个refresh_token给用户。用户可以使用这个刷新码来请求信的通行码。Kong默认每个刷新码只能使用一次。
REQUEST:

1
2
3
4
5
6
7
curl -X POST \
--url "https://localhost:8443/demo/oauth2/token" \
--data "grant_type=refresh_token" \
--data "client_id=oauth2-demo-client-id" \
--data "client_secret=oauth2-demo-client-secret" \
--data "refresh_token=<REFRESH_TOKEN>" \
--insecure

RESPONSE:

1
2
3
4
5
6
{
"refresh_token": "AbvtChlNibVCyQxmf0Ue0lQ9fzXLCNQv",
"token_type": "bearer",
"access_token": "d3n0lHkrvYi6CDolsDWdMs0lZ8PblFyQ",
"expires_in": 7200
}