Skip to content

Commit

Permalink
feat: add support for password grant in keycloak plugin (#6586)
Browse files Browse the repository at this point in the history
Co-authored-by: rushabh-sukhadia <[email protected]>
  • Loading branch information
azilentech and Rushabh-Sukhadia authored Mar 21, 2022
1 parent a47dab7 commit 66e944c
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 4 deletions.
94 changes: 90 additions & 4 deletions apisix/plugins/authz-keycloak.lua
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,12 @@ local schema = {
access_token_expires_leeway = {type = "integer", minimum = 0, default = 0},
refresh_token_expires_in = {type = "integer", minimum = 1, default = 3600},
refresh_token_expires_leeway = {type = "integer", minimum = 0, default = 0},
},
password_grant_token_generation_incoming_uri = {
type = "string",
minLength = 1,
maxLength = 4096
},
},
allOf = {
-- Require discovery or token endpoint.
{
Expand Down Expand Up @@ -375,7 +380,7 @@ local function authz_keycloak_ensure_sa_access_token(conf)

local params = {
method = "POST",
body = ngx.encode_args({
body = ngx.encode_args({
grant_type = "refresh_token",
client_id = client_id,
client_secret = conf.client_secret,
Expand Down Expand Up @@ -451,7 +456,7 @@ local function authz_keycloak_ensure_sa_access_token(conf)

local params = {
method = "POST",
body = ngx.encode_args({
body = ngx.encode_args({
grant_type = "client_credentials",
client_id = client_id,
client_secret = conf.client_secret,
Expand Down Expand Up @@ -639,7 +644,7 @@ local function evaluate_permissions(conf, ctx, token)

local params = {
method = "POST",
body = ngx.encode_args({
body = ngx.encode_args({
grant_type = conf.grant_type,
audience = authz_keycloak_get_client_id(conf),
response_mode = "decision",
Expand Down Expand Up @@ -695,8 +700,89 @@ local function fetch_jwt_token(ctx)
return token
end

-- To get new access token by calling get token api
local function generate_token_using_password_grant(conf,ctx)
log.debug("generate_token_using_password_grant Function Called")

local body, err = core.request.get_body()
if err or not body then
log.error("Failed to get request body: ", err)
return 503
end
local parameters = ngx.decode_args(body)

local username = parameters["username"]
local password = parameters["password"]

if not username then
local err = "username is missing."
log.error(err)
return 422, err
end
if not password then
local err = "password is missing."
log.error(err)
return 422, err
end

local client_id = authz_keycloak_get_client_id(conf)

local token_endpoint = authz_keycloak_get_token_endpoint(conf)

if not token_endpoint then
local err = "Unable to determine token endpoint."
log.error(err)
return 503, err
end
local httpc = authz_keycloak_get_http_client(conf)

local params = {
method = "POST",
body = ngx.encode_args({
grant_type = "password",
client_id = client_id,
client_secret = conf.client_secret,
username = username,
password = password
}),
headers = {
["Content-Type"] = "application/x-www-form-urlencoded"
}
}

params = authz_keycloak_configure_params(params, conf)

local res, err = httpc:request_uri(token_endpoint, params)

if not res then
err = "Accessing token endpoint URL (" .. token_endpoint
.. ") failed: " .. err
log.error(err)
return 401, {message = err}
end

log.debug("Response data: " .. res.body)
local json, err = authz_keycloak_parse_json_response(res)

if not json then
err = "Could not decode JSON from response"
.. (err and (": " .. err) or '.')
log.error(err)
return 401, {message = err}
end

return res.status, res.body
end

function _M.access(conf, ctx)
local headers = core.request.headers(ctx)
local need_grant_token = conf.password_grant_token_generation_incoming_uri and
ctx.var.request_uri == conf.password_grant_token_generation_incoming_uri and
headers["content-type"] == "application/x-www-form-urlencoded" and
core.request.get_method() == "POST"
if need_grant_token then
return generate_token_using_password_grant(conf,ctx)
end
log.debug("hit keycloak-auth access")
local jwt_token, err = fetch_jwt_token(ctx)
if not jwt_token then
Expand Down
22 changes: 22 additions & 0 deletions docs/en/latest/plugins/authz-keycloak.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ For more information on Keycloak, refer to [Keycloak Authorization Docs](https:/
| keepalive_timeout | integer | optional | 60000 | positive integer >= 1000 | Idle timeout after which established HTTP connections will be closed. |
| keepalive_pool | integer | optional | 5 | positive integer >= 1 | Maximum number of connections in the connection pool. |
| access_denied_redirect_uri | string | optional | | [1, 2048] | Redirect unauthorized user with the given uri like "http://127.0.0.1/test", instead of returning `"error_description":"not_authorized"`. |
| password_grant_token_generation_incoming_uri | string | optional | | /api/token | You can set this uri value to generate token using password grant type. Plugin will compare incoming request uri with this value. |

### Discovery and Endpoints

Expand Down Expand Up @@ -122,6 +123,27 @@ of the same name. The scope is then added to every permission to check.
If `lazy_load_paths` is `false`, the plugin adds the mapped scope to any of the static permissions configured
in the `permissions` attribute, even if they contain one or more scopes already.

### Password Grant Token Generation Incoming URI

If you want to generate a token using `password` grant, you can set the value of `password_grant_token_generation_incoming_uri`.

Incoming request URI will be matched with this value and if matched, it will generate a token using `Token Endpoint`.
It will also check if the request method is `POST`.

You need to pass `application/x-www-form-urlencoded` as `Content-Type` header and `username`, `password` as parameters.

**Sample request**

If value of `password_grant_token_generation_incoming_uri` is `/api/token`, you can use following curl request.

```shell
curl --location --request POST 'http://127.0.0.1:9080/api/token' \
--header 'Accept: application/json, text/plain, */*' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'username=<User_Name>' \
--data-urlencode 'password=<Password>'
```

## How To Enable

Create a `route` and enable the `authz-keycloak` plugin on the route:
Expand Down
75 changes: 75 additions & 0 deletions t/plugin/authz-keycloak.t
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ done
access_token_expires_leeway = 0,
refresh_token_expires_in = 3600,
refresh_token_expires_leeway = 0,
password_grant_token_generation_incoming_uri = "/api/token",
})
if not ok then
ngx.say(err)
Expand Down Expand Up @@ -548,3 +549,77 @@ GET /t
--- response_headers
Location: http://127.0.0.1/test
--- error_code: 307



=== TEST 18: Add https endpoint with password_grant_token_generation_incoming_uri
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"authz-keycloak": {
"token_endpoint": "https://127.0.0.1:8443/auth/realms/University/protocol/openid-connect/token",
"permissions": ["course_resource#view"],
"client_id": "course_management",
"client_secret": "d1ec69e9-55d2-4109-a3ea-befa071579d5",
"grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket",
"timeout": 3000,
"ssl_verify": false,
"password_grant_token_generation_incoming_uri": "/api/token"
}
},
"upstream": {
"nodes": {
"127.0.0.1:1982": 1
},
"type": "roundrobin"
},
"uri": "/api/token"
}]]
)

if code >= 300 then
ngx.status = code
end

local json_decode = require("toolkit.json").decode
local http = require "resty.http"
local httpc = http.new()
local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/api/token"
local res, err = httpc:request_uri(uri, {
method = "POST",
headers = {
["Content-Type"] = "application/x-www-form-urlencoded",
},

body = ngx.encode_args({
username = "teacher@gmail.com",
password = "123456",
}),
})

if res.status == 200 then
local body = json_decode(res.body)
local accessToken = body["access_token"]
local refreshToken = body["refresh_token"]

if accessToken and refreshToken then
ngx.say(true)
else
ngx.say(false)
end
else
ngx.say(false)
end
}
}
--- request
GET /t
--- response_body
true
--- no_error_log
[error]

0 comments on commit 66e944c

Please sign in to comment.