Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for password grant in authz-keycloak plugin #6586

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 @@ -621,3 +622,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]
Loading