在之前的文章中已经介绍了Kong这个api网关的安装和基本打开方式。这篇文章介绍一下kong在某个Route
或Service
中使用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_id
和client_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_code
和password
grant会返回一个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 }
|