Skip to content

Commit

Permalink
feature: support authorization Plugin for Keycloak Identity Server (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
sshniro authored Jun 16, 2020
1 parent 8b4ae7c commit cd98a2b
Show file tree
Hide file tree
Showing 9 changed files with 656 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .travis/linux_openresty_runner.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ before_install() {
docker pull redis:3.0-alpine
docker run --rm -itd -p 6379:6379 --name apisix_redis redis:3.0-alpine
docker run --rm -itd -e HTTP_PORT=8888 -e HTTPS_PORT=9999 -p 8888:8888 -p 9999:9999 mendhak/http-https-echo
# Runs Keycloak version 10.0.2 with inbuilt policies for unit tests
docker run --rm -itd -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 sshniro/keycloak-apisix
# spin up kafka cluster for tests (1 zookeper and 1 kafka instance)
docker pull bitnami/zookeeper:3.6.0
docker pull bitnami/kafka:latest
Expand Down
2 changes: 2 additions & 0 deletions .travis/linux_tengine_runner.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ before_install() {
docker pull redis:3.0-alpine
docker run --rm -itd -p 6379:6379 --name apisix_redis redis:3.0-alpine
docker run --rm -itd -e HTTP_PORT=8888 -e HTTPS_PORT=9999 -p 8888:8888 -p 9999:9999 mendhak/http-https-echo
# Runs Keycloak version 10.0.2 with inbuilt policies for unit tests
docker run --rm -itd -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 sshniro/keycloak-apisix
# spin up kafka cluster for tests (1 zookeper and 1 kafka instance)
docker pull bitnami/zookeeper:3.6.0
docker pull bitnami/kafka:latest
Expand Down
165 changes: 165 additions & 0 deletions apisix/plugins/authz-keycloak.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
local core = require("apisix.core")
local http = require "resty.http"
local sub_str = string.sub
local url = require "net.url"
local tostring = tostring
local ngx = ngx
local plugin_name = "authz-keycloak"


local schema = {
type = "object",
properties = {
token_endpoint = {type = "string", minLength = 1, maxLength = 4096},
permissions = {
type = "array",
items = {
type = "string",
minLength = 1, maxLength = 100
},
uniqueItems = true
},
grant_type = {
type = "string",
default="urn:ietf:params:oauth:grant-type:uma-ticket",
enum = {"urn:ietf:params:oauth:grant-type:uma-ticket"},
minLength = 1, maxLength = 100
},
audience = {type = "string", minLength = 1, maxLength = 100},
timeout = {type = "integer", minimum = 1000, default = 3000},
policy_enforcement_mode = {
type = "string",
enum = {"ENFORCING", "PERMISSIVE"},
default = "ENFORCING"
},
keepalive = {type = "boolean", default = true},
keepalive_timeout = {type = "integer", minimum = 1000, default = 60000},
keepalive_pool = {type = "integer", minimum = 1, default = 5},

},
required = {"token_endpoint"}
}


local _M = {
version = 0.1,
priority = 2000,
type = 'auth',
name = plugin_name,
schema = schema,
}

function _M.check_schema(conf)
return core.schema.check(schema, conf)
end

local function is_path_protected(conf)
-- TODO if permissions are empty lazy load paths from Keycloak
if conf.permissions == nil then
return false
end
return true
end


local function evaluate_permissions(conf, token)
local url_decoded = url.parse(conf.token_endpoint)
local host = url_decoded.host
local port = url_decoded.port

if not port then
if url_decoded.scheme == "https" then
port = 443
else
port = 80
end
end

if not is_path_protected(conf) and conf.policy_enforcement_mode == "ENFORCING" then
core.response.exit(403)
return
end

local httpc = http.new()
httpc:set_timeout(conf.timeout)

local params = {
method = "POST",
body = ngx.encode_args({
grant_type = conf.grant_type,
audience = conf.audience,
response_mode = "decision",
permission = conf.permissions
}),
headers = {
["Content-Type"] = "application/x-www-form-urlencoded",
["Authorization"] = token
}
}

if conf.keepalive then
params.keepalive_timeout = conf.keepalive_timeout
params.keepalive_pool = conf.keepalive_pool
else
params.keepalive = conf.keepalive
end

local httpc_res, httpc_err = httpc:request_uri(conf.token_endpoint, params)

if not httpc_res then
core.log.error("error while sending authz request to [", host ,"] port[",
tostring(port), "] ", httpc_err)
core.response.exit(500, httpc_err)
return
end

if httpc_res.status >= 400 then
core.log.error("status code: ", httpc_res.status, " msg: ", httpc_res.body)
core.response.exit(httpc_res.status, httpc_res.body)
end
end


local function fetch_jwt_token(ctx)
local token = core.request.header(ctx, "authorization")
if not token then
return nil, "authorization header not available"
end

local prefix = sub_str(token, 1, 7)
if prefix ~= 'Bearer ' and prefix ~= 'bearer ' then
return "Bearer " .. token
end
return token
end


function _M.rewrite(conf, ctx)
core.log.debug("hit keycloak-auth rewrite")
local jwt_token, err = fetch_jwt_token(ctx)
if not jwt_token then
core.log.error("failed to fetch JWT token: ", err)
return 401, {message = "Missing JWT token in request"}
end

evaluate_permissions(conf, jwt_token)
end


return _M
1 change: 1 addition & 0 deletions conf/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ plugins: # plugin list
- http-logger
- skywalking
- echo
- authz-keycloak

stream_plugins:
- mqtt-proxy
Binary file added doc/images/plugin/authz-keycloak.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
135 changes: 135 additions & 0 deletions doc/plugins/authz-keycloak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<!--
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
-->

[Chinese](authz-keycloak-cn.md)

# Summary
- [**Name**](#name)
- [**Attributes**](#attributes)
- [**How To Enable**](#how-to-enable)
- [**Test Plugin**](#test-plugin)
- [**Disable Plugin**](#disable-plugin)
- [**Examples**](#examples)


## Name

`authz-keycloak` is an authorization plugin to be used with the Keycloak Identity Server. Keycloak is an OAuth/OIDC and
UMA compliant Ideneity Server. Although, its developed to working in conjunction with Keycloak it should work with any
OAuth/OIDC and UMA compliant identity providers as well.

For more information on JWT, refer to [Keycloak Authorization Docs](https://www.keycloak.org/docs/latest/authorization_services) for more information.

## Attributes

|Name |Requirement |Description|
|--------- |-------- |-----------|
| token_endpoint|required |A OAuth2-compliant Token Endpoint that supports the urn:ietf:params:oauth:grant-type:uma-ticket grant type.|
| grant_type |optional |Default value is `urn:ietf:params:oauth:grant-type:uma-ticket`.|
| audience |optional |The client identifier of the resource server to which the client is seeking access. This parameter is mandatory in case the permission parameter is defined.|
| permissions |optional |This parameter is optional. A string representing a set of one or more resources and scopes the client is seeking access. The format of the string must be: RESOURCE_ID#SCOPE_ID.|
| timeout |optional |Timeout for the http connection with the Identity Server. Default is 3 seconds|
| policy_enforcement_mode|required |Enforcing or Permissive.|


### Policy Enforcement Mode

Specifies how policies are enforced when processing authorization requests sent to the server.

**Enforcing**

- (default mode) Requests are denied by default even when there is no policy associated with a given resource.

**Permissive**

- Requests are allowed even when there is no policy associated with a given resource.


## How To Enable

Create a route and enable the authz-keycloak plugin on the route:

```shell
curl http://127.0.0.1:9080/apisix/admin/routes/5 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"uri": "/get",
"plugins": {
"authz-keycloak": {
"token_endpoint": "http://127.0.0.1:8090/auth/realms/{client_id}/protocol/openid-connect/token",
"permissions": ["resource name#scope name"],
"audience": "Client ID"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:8080": 1
}
}
}
```
## Test Plugin
```shell
curl http://127.0.0.1:9080/get -H 'Authorization: Bearer {JWT Token}'
```
## Disable Plugin
Remove the corresponding json configuration in the plugin configuration to disable the `authz-keycloak`.
APISIX plugins are hot-reloaded, therefore no need to restart APISIX.
```shell
curl http://127.0.0.1:9080/apisix/admin/routes/5 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"uri": "/get",
"plugins": {
},
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:8080": 1
}
}
}
```

## Examples

Checkout the unit test for of the authz-keycloak.t to understand how the authorization policies can be integrated into your
API workflows. Run the following docker image and visit `http://localhost:8090` to view the associated policies for the unit tests.

```bash
docker run -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 sshniro/keycloak-apisix
```

The following image shows how the policies are configures in the Keycloak server.

![Keycloak policy design](../images/plugin/authz-keycloak.png)

## Future Development

- Currently the authz-plugin requires to define the resource name and required scopes inorder to enforce policies for the routes.
However, Keycloak's official adapters (Java, JS) also provides path matching by querying Keycloak paths dynamically, and
lazy loading the paths to identify resources. Future version on authz-plugin will support this functionality.

- Support to read scope and configurations from the Keycloak JSON File
2 changes: 1 addition & 1 deletion t/admin/plugins.t
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ __DATA__
--- request
GET /apisix/admin/plugins/list
--- response_body_like eval
qr/\["limit-req","limit-count","limit-conn","key-auth","basic-auth","prometheus","node-status","jwt-auth","zipkin","ip-restriction","grpc-transcode","serverless-pre-function","serverless-post-function","openid-connect","proxy-rewrite","redirect","response-rewrite","fault-injection","udp-logger","wolf-rbac","proxy-cache","tcp-logger","proxy-mirror","kafka-logger","cors","consumer-restriction","syslog","batch-requests","http-logger","skywalking","echo"\]/
qr/\["limit-req","limit-count","limit-conn","key-auth","basic-auth","prometheus","node-status","jwt-auth","zipkin","ip-restriction","grpc-transcode","serverless-pre-function","serverless-post-function","openid-connect","proxy-rewrite","redirect","response-rewrite","fault-injection","udp-logger","wolf-rbac","proxy-cache","tcp-logger","proxy-mirror","kafka-logger","cors","consumer-restriction","syslog","batch-requests","http-logger","skywalking","echo","authz-keycloak"\]/
--- no_error_log
[error]
Expand Down
1 change: 1 addition & 0 deletions t/debug/debug-mode.t
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ loaded plugin and sort by priority: 2520 name: basic-auth
loaded plugin and sort by priority: 2510 name: jwt-auth
loaded plugin and sort by priority: 2500 name: key-auth
loaded plugin and sort by priority: 2400 name: consumer-restriction
loaded plugin and sort by priority: 2000 name: authz-keycloak
loaded plugin and sort by priority: 1010 name: proxy-mirror
loaded plugin and sort by priority: 1009 name: proxy-cache
loaded plugin and sort by priority: 1008 name: proxy-rewrite
Expand Down
Loading

0 comments on commit cd98a2b

Please sign in to comment.