From f952324bac44c00642694b0213d69749a91dff16 Mon Sep 17 00:00:00 2001 From: rushabh-sukhadia Date: Fri, 11 Mar 2022 12:42:50 +0530 Subject: [PATCH 01/11] Removed unnecessary file (+2 squashed commit) Squashed commit: [99ff9f43] refactor: Removing unnecessary properties (#6574) Removing unnecessary properties and test codes. And also improve document for Token generation feature. feat: #6574 [22c8d8eb] feat: Support of password grant type for token generation (#6574) Committing changes after adding new feature for generating token based on `user id/password` and also taking support of grant type `password` feat: #6574 --- apisix/plugins/authz-keycloak.lua | 100 ++++++++++++++++++++++- docs/en/latest/plugins/authz-keycloak.md | 21 +++++ 2 files changed, 120 insertions(+), 1 deletion(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index e1a83c520472..8bc3d66720b8 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -67,7 +67,8 @@ 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}, -}, + token_generation_endpoint = {type = "string", minLength = 1, maxLength = 4096}, + }, allOf = { -- Require discovery or token endpoint. { @@ -694,9 +695,106 @@ local function fetch_jwt_token(ctx) end return token end +-- This function is used to split data from array by respective delimitter and return array in result +local function stringsplit(s, delimiter) + local result = {}; + for match in (s..delimiter):gmatch("(.-)"..delimiter) do + table.insert(result, match); + end + return result; +end +-- To get new access token by calling get token api +local function generate_token(conf,ctx) + log.warn("------------Generate Access Token Function Called---------------") + --Read Body + ngx.req.read_body(); --To read requestbody first + --Get Body Data + local RequestBody=ngx.req.get_body_data(); + local UserName = ""; local Password = ""; + --split by & + local strBodyArr = stringsplit(RequestBody, "&"); + if strBodyArr then + for k, strBodyValue in ipairs(strBodyArr) do + if string.find(strBodyValue, "username") then + --split by = + local usr = stringsplit(strBodyValue, "="); + UserName = usr[2]; + end + if string.find(strBodyValue, "password") then + local psw = stringsplit(strBodyValue, "="); + Password = psw[2]; + end + end + end + + local client_id = authz_keycloak_get_client_id(conf) + + local token_endpoint = authz_keycloak_get_token_endpoint(conf) + + if not token_endpoint then + log.error("Unable to determine token endpoint.") + return 500, "Unable to determine token endpoint." + 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) + + log.warn(core.json.encode(json)) + if not json then + err = "Could not decode JSON from token endpoint" + .. (err and (": " .. err) or '.') + log.error(err) + return 401, {message = err} + end + + return res.status, res.body; +end function _M.access(conf, ctx) + -- Get Requested URI + local RequestURI = string.upper(ngx.var.request_uri); + + if conf.token_generation_endpoint then + -- Get Token generation end point of key cloak which we have mession in keyclock plugin config + local token_generation_endpoint = string.upper(conf.token_generation_endpoint); + local curr_req_method = string.upper(ctx.curr_req_matched["_method"]); + --Call Generate Access Token function if "\Token" found in URI based on configuration + if RequestURI == token_generation_endpoint then + if curr_req_method ~= "POST" then + log.error("Invalid request type") + return 400, {message = "Request method must be POST only.!"} + end + + return generate_token(conf,ctx); + end + end log.debug("hit keycloak-auth access") local jwt_token, err = fetch_jwt_token(ctx) if not jwt_token then diff --git a/docs/en/latest/plugins/authz-keycloak.md b/docs/en/latest/plugins/authz-keycloak.md index 89ae6599cb8d..7e5c7f20252a 100644 --- a/docs/en/latest/plugins/authz-keycloak.md +++ b/docs/en/latest/plugins/authz-keycloak.md @@ -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"`. | +| token_generation_endpoint | string | optional | | | Endpoint path to identify URL pattern based on Path configuration. So that we can generate token if Request Uri match with this path. | ### Discovery and Endpoints @@ -122,6 +123,26 @@ 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. +### Token generation endpoint + +If user wants to generate a token based on user name and password with the support of grant type `password`. + +The user have to configure URI path (E.g. `/api/Token`) in `token_generation_endpoint` which will match with incomming Request URI path and it will generate a new token with using `token_endpoint`. + +And for other route config, if will check token and redirect to the resource which are allocated to users. + +The user must have to pass Content-Type header as `application/x-www-form-urlencoded` and `username & password` in body part of request. + +## Token generation example + +```cURL Code +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: From fc9610298f8ab65d361ea42bddb4dcded1e9bbfe Mon Sep 17 00:00:00 2001 From: azilentech Date: Sat, 12 Mar 2022 11:02:48 +0530 Subject: [PATCH 02/11] Refactored code. Better variable names. Documentation updated as per guideline. --- apisix/plugins/authz-keycloak.lua | 153 ++++++++++++----------- docs/en/latest/plugins/authz-keycloak.md | 24 ++-- 2 files changed, 94 insertions(+), 83 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 8bc3d66720b8..4581ba613cfd 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -67,7 +67,11 @@ 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}, - token_generation_endpoint = {type = "string", minLength = 1, maxLength = 4096}, + password_grant_token_generation_incoming_uri = { + type = "string", + minLength = 1, + maxLength = 4096 + }, }, allOf = { -- Require discovery or token endpoint. @@ -695,104 +699,109 @@ local function fetch_jwt_token(ctx) end return token end --- This function is used to split data from array by respective delimitter and return array in result -local function stringsplit(s, delimiter) - local result = {}; + +local function split_string(s, delimiter) + local result = {} for match in (s..delimiter):gmatch("(.-)"..delimiter) do - table.insert(result, match); + table.insert(result, match) end - return result; + return result end -- To get new access token by calling get token api -local function generate_token(conf,ctx) - log.warn("------------Generate Access Token Function Called---------------") +local function generate_token_using_password_grant(conf,ctx) + log.warn("generate_token_using_password_grant Function Called") --Read Body - ngx.req.read_body(); --To read requestbody first + ngx.req.read_body() --Get Body Data - local RequestBody=ngx.req.get_body_data(); - local UserName = ""; local Password = ""; + local request_body=ngx.req.get_body_data() + local username = nil + local password = nil --split by & - local strBodyArr = stringsplit(RequestBody, "&"); - if strBodyArr then - for k, strBodyValue in ipairs(strBodyArr) do - if string.find(strBodyValue, "username") then + local parameters_array = split_string(request_body, "&") + if parameters_array then + for k, parameter in ipairs(parameters_array) do + if string.find(parameter, "username") then --split by = - local usr = stringsplit(strBodyValue, "="); - UserName = usr[2]; + local username_value_array = split_string(parameter, "=") + username = username_value_array[2] end - if string.find(strBodyValue, "password") then - local psw = stringsplit(strBodyValue, "="); - Password = psw[2]; + if string.find(parameter, "password") then + --split by = + local password_value_array = split_string(parameter, "=") + password = password_value_array[2] end end end + 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 - log.error("Unable to determine token endpoint.") - return 500, "Unable to determine token endpoint." + local err = "Unable to determine token endpoint." + log.error(err) + return 500, err end - local httpc = authz_keycloak_get_http_client(conf) + 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" - } + 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) + params = authz_keycloak_configure_params(params, conf) - local res, err = httpc:request_uri(token_endpoint, params) + 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) - - log.warn(core.json.encode(json)) - if not json then - err = "Could not decode JSON from token endpoint" - .. (err and (": " .. err) or '.') - log.error(err) - return 401, {message = err} - end - - return res.status, res.body; + 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) - -- Get Requested URI - local RequestURI = string.upper(ngx.var.request_uri); - - if conf.token_generation_endpoint then - -- Get Token generation end point of key cloak which we have mession in keyclock plugin config - local token_generation_endpoint = string.upper(conf.token_generation_endpoint); - local curr_req_method = string.upper(ctx.curr_req_matched["_method"]); - --Call Generate Access Token function if "\Token" found in URI based on configuration - if RequestURI == token_generation_endpoint then - if curr_req_method ~= "POST" then - log.error("Invalid request type") - return 400, {message = "Request method must be POST only.!"} - end - return generate_token(conf,ctx); + if conf.password_grant_token_generation_incoming_uri then + if string.upper(ngx.var.request_uri) + == string.upper(conf.password_grant_token_generation_incoming_uri) then + if string.upper(ctx.curr_req_matched["_method"]) == "POST" then + return generate_token_using_password_grant(conf,ctx) + end end end log.debug("hit keycloak-auth access") diff --git a/docs/en/latest/plugins/authz-keycloak.md b/docs/en/latest/plugins/authz-keycloak.md index 7e5c7f20252a..3039c3b4ccbd 100644 --- a/docs/en/latest/plugins/authz-keycloak.md +++ b/docs/en/latest/plugins/authz-keycloak.md @@ -55,7 +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"`. | -| token_generation_endpoint | string | optional | | | Endpoint path to identify URL pattern based on Path configuration. So that we can generate token if Request Uri match with this path. | +| 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 @@ -123,24 +123,26 @@ 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. -### Token generation endpoint -If user wants to generate a token based on user name and password with the support of grant type `password`. +### Password Grant Token Generation Incoming URI -The user have to configure URI path (E.g. `/api/Token`) in `token_generation_endpoint` which will match with incomming Request URI path and it will generate a new token with using `token_endpoint`. +If you want to generate a token using `password` grant, you can set value of `password_grant_token_generation_incoming_uri`. -And for other route config, if will check token and redirect to the resource which are allocated to users. +Incoming request URI will be matched with this value and if matched, it will generate token using `Token Endpoint`. +It will also check, if REST method is `POST`. -The user must have to pass Content-Type header as `application/x-www-form-urlencoded` and `username & password` in body part of request. +You need to pass `application/x-www-form-urlencoded` as `Content-Type` header and `username`, `password` as parameters. -## Token generation example +**Sample request** -```cURL Code -curl --location --request POST 'http://127.0.0.1:9080/api/Token' \ +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' +--data-urlencode 'username=' \ +--data-urlencode 'password=' ``` ## How To Enable From 07ae576e4a3e7edc761564eae0f5313a22ca9404 Mon Sep 17 00:00:00 2001 From: azilentech Date: Sat, 12 Mar 2022 22:56:20 +0530 Subject: [PATCH 03/11] Test cases added. --- t/plugin/authz-keycloak.t | 114 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index f844754a4415..b13b91814cc5 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -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) @@ -621,3 +622,116 @@ 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#delete"], + "client_id": "course_management", + "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", + "timeout": 3000, + "password_grant_token_generation_incoming_uri": "/api/token" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello1" + }]], + [[{ + "node": { + "value": { + "plugins": { + "authz-keycloak": { + "token_endpoint": "https://127.0.0.1:8443/auth/realms/University/protocol/openid-connect/token", + "permissions": ["course_resource#delete"], + "client_id": "course_management", + "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", + "timeout": 3000, + "password_grant_token_generation_incoming_uri": "/api/token" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello1" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 19: test for generating token via password grant +--- config + location /t { + content_by_lua_block { + 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] \ No newline at end of file From a2b0d4051e765bdcd4a900d631be0e242d69f279 Mon Sep 17 00:00:00 2001 From: azilentech Date: Sat, 12 Mar 2022 23:42:17 +0530 Subject: [PATCH 04/11] Used apisix util split method. --- apisix/plugins/authz-keycloak.lua | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 4581ba613cfd..fa46b2a00554 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -19,10 +19,12 @@ local http = require "resty.http" local sub_str = string.sub local type = type local ngx = ngx +local util = require("apisix.cli.util") local plugin_name = "authz-keycloak" local log = core.log local pairs = pairs +local ipairs = ipairs local schema = { type = "object", @@ -700,14 +702,6 @@ local function fetch_jwt_token(ctx) return token end -local function split_string(s, delimiter) - local result = {} - for match in (s..delimiter):gmatch("(.-)"..delimiter) do - table.insert(result, match) - end - return result -end - -- To get new access token by calling get token api local function generate_token_using_password_grant(conf,ctx) log.warn("generate_token_using_password_grant Function Called") @@ -718,18 +712,23 @@ local function generate_token_using_password_grant(conf,ctx) local username = nil local password = nil --split by & - local parameters_array = split_string(request_body, "&") - if parameters_array then + local parameters_array = util.split(request_body, "&") + + if #parameters_array == 2 then for k, parameter in ipairs(parameters_array) do if string.find(parameter, "username") then --split by = - local username_value_array = split_string(parameter, "=") - username = username_value_array[2] + local username_value_array = util.split(parameter, "=") + if #username_value_array == 2 then + username = username_value_array[2] + end end if string.find(parameter, "password") then --split by = - local password_value_array = split_string(parameter, "=") - password = password_value_array[2] + local password_value_array = util.split(parameter, "=") + if #password_value_array == 2 then + password = password_value_array[2] + end end end end From fbdf244cd52e47294732e300e29270fcf31a4f5e Mon Sep 17 00:00:00 2001 From: azilentech Date: Sun, 13 Mar 2022 02:04:07 +0530 Subject: [PATCH 05/11] I have combined test cases where route is set and token is generated using password_grant_token_generation_incoming_uri. I have removed some more errors reported by git workflow build process. --- apisix/plugins/authz-keycloak.lua | 6 ++--- docs/en/latest/plugins/authz-keycloak.md | 1 - t/plugin/authz-keycloak.t | 30 +++++++----------------- 3 files changed, 12 insertions(+), 25 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index fa46b2a00554..dcdb6baf2c67 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -796,9 +796,9 @@ end function _M.access(conf, ctx) if conf.password_grant_token_generation_incoming_uri then - if string.upper(ngx.var.request_uri) - == string.upper(conf.password_grant_token_generation_incoming_uri) then - if string.upper(ctx.curr_req_matched["_method"]) == "POST" then + if ngx.var.request_uri:upper() + == conf.password_grant_token_generation_incoming_uri:upper() then + if ctx.curr_req_matched["_method"]:upper() == "POST" then return generate_token_using_password_grant(conf,ctx) end end diff --git a/docs/en/latest/plugins/authz-keycloak.md b/docs/en/latest/plugins/authz-keycloak.md index 3039c3b4ccbd..67618eaaf29f 100644 --- a/docs/en/latest/plugins/authz-keycloak.md +++ b/docs/en/latest/plugins/authz-keycloak.md @@ -123,7 +123,6 @@ 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 value of `password_grant_token_generation_incoming_uri`. diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index b13b91814cc5..5c4a8298bc64 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -636,10 +636,12 @@ Location: http://127.0.0.1/test "plugins": { "authz-keycloak": { "token_endpoint": "https://127.0.0.1:8443/auth/realms/University/protocol/openid-connect/token", - "permissions": ["course_resource#delete"], + "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" } }, @@ -649,7 +651,7 @@ Location: http://127.0.0.1/test }, "type": "roundrobin" }, - "uri": "/hello1" + "uri": "/api/token" }]], [[{ "node": { @@ -657,10 +659,12 @@ Location: http://127.0.0.1/test "plugins": { "authz-keycloak": { "token_endpoint": "https://127.0.0.1:8443/auth/realms/University/protocol/openid-connect/token", - "permissions": ["course_resource#delete"], + "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" } }, @@ -670,7 +674,7 @@ Location: http://127.0.0.1/test }, "type": "roundrobin" }, - "uri": "/hello1" + "uri": "/api/token" }, "key": "/apisix/routes/1" }, @@ -681,22 +685,7 @@ Location: http://127.0.0.1/test if code >= 300 then ngx.status = code end - ngx.say(body) - } - } ---- request -GET /t ---- response_body -passed ---- no_error_log -[error] - - -=== TEST 19: test for generating token via password grant ---- config - location /t { - content_by_lua_block { local json_decode = require("toolkit.json").decode local http = require "resty.http" local httpc = http.new() @@ -726,7 +715,6 @@ passed else ngx.say(false) end - } } --- request @@ -734,4 +722,4 @@ GET /t --- response_body true --- no_error_log -[error] \ No newline at end of file +[error] From 250040b0e5e3d1155c50486a9128acd5fe55e61e Mon Sep 17 00:00:00 2001 From: azilentech Date: Sun, 13 Mar 2022 22:22:01 +0530 Subject: [PATCH 06/11] used local str_find --- apisix/plugins/authz-keycloak.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index dcdb6baf2c67..e3475720a710 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -17,6 +17,7 @@ local core = require("apisix.core") local http = require "resty.http" local sub_str = string.sub +local str_find = string.find local type = type local ngx = ngx local util = require("apisix.cli.util") @@ -716,14 +717,14 @@ local function generate_token_using_password_grant(conf,ctx) if #parameters_array == 2 then for k, parameter in ipairs(parameters_array) do - if string.find(parameter, "username") then + if str_find(parameter, "username") then --split by = local username_value_array = util.split(parameter, "=") if #username_value_array == 2 then username = username_value_array[2] end end - if string.find(parameter, "password") then + if str_find(parameter, "password") then --split by = local password_value_array = util.split(parameter, "=") if #password_value_array == 2 then From eb60718f8af671285a7039c9c49713f8c6d69e57 Mon Sep 17 00:00:00 2001 From: azilentech Date: Mon, 14 Mar 2022 10:37:57 +0530 Subject: [PATCH 07/11] Review comments incorporated. Used ngx.decode_args for input arguments. --- apisix/plugins/authz-keycloak.lua | 58 ++++++++++--------------------- 1 file changed, 18 insertions(+), 40 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index e3475720a710..81b9c5484292 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -17,15 +17,12 @@ local core = require("apisix.core") local http = require "resty.http" local sub_str = string.sub -local str_find = string.find local type = type local ngx = ngx -local util = require("apisix.cli.util") local plugin_name = "authz-keycloak" local log = core.log local pairs = pairs -local ipairs = ipairs local schema = { type = "object", @@ -705,42 +702,25 @@ end -- To get new access token by calling get token api local function generate_token_using_password_grant(conf,ctx) - log.warn("generate_token_using_password_grant Function Called") - --Read Body - ngx.req.read_body() - --Get Body Data - local request_body=ngx.req.get_body_data() - local username = nil - local password = nil - --split by & - local parameters_array = util.split(request_body, "&") - - if #parameters_array == 2 then - for k, parameter in ipairs(parameters_array) do - if str_find(parameter, "username") then - --split by = - local username_value_array = util.split(parameter, "=") - if #username_value_array == 2 then - username = username_value_array[2] - end - end - if str_find(parameter, "password") then - --split by = - local password_value_array = util.split(parameter, "=") - if #password_value_array == 2 then - password = password_value_array[2] - end - end - end + 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" + local err = "username is missing." log.error(err) return 422, err end if not password then - local err = "password is missing" + local err = "password is missing." log.error(err) return 422, err end @@ -791,18 +771,16 @@ local function generate_token_using_password_grant(conf,ctx) return 401, {message = err} end - return res.status, res.body + return res.status, res.body end function _M.access(conf, ctx) - if conf.password_grant_token_generation_incoming_uri then - if ngx.var.request_uri:upper() - == conf.password_grant_token_generation_incoming_uri:upper() then - if ctx.curr_req_matched["_method"]:upper() == "POST" then - return generate_token_using_password_grant(conf,ctx) - end - end + if conf.password_grant_token_generation_incoming_uri and + ngx.var.request_uri:upper() == + conf.password_grant_token_generation_incoming_uri:upper() and + ctx.curr_req_matched["_method"]:upper() == "POST" then + return generate_token_using_password_grant(conf,ctx) end log.debug("hit keycloak-auth access") local jwt_token, err = fetch_jwt_token(ctx) From 7cea98239ea582d45622b70b85a55eb0536828f3 Mon Sep 17 00:00:00 2001 From: azilentech Date: Tue, 15 Mar 2022 23:56:29 +0530 Subject: [PATCH 08/11] Review comments incorporated. --- apisix/plugins/authz-keycloak.lua | 3 +-- docs/en/latest/plugins/authz-keycloak.md | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 81b9c5484292..338eeea66204 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -775,11 +775,10 @@ local function generate_token_using_password_grant(conf,ctx) end function _M.access(conf, ctx) - if conf.password_grant_token_generation_incoming_uri and ngx.var.request_uri:upper() == conf.password_grant_token_generation_incoming_uri:upper() and - ctx.curr_req_matched["_method"]:upper() == "POST" then + core.request.get_method() == "POST" then return generate_token_using_password_grant(conf,ctx) end log.debug("hit keycloak-auth access") diff --git a/docs/en/latest/plugins/authz-keycloak.md b/docs/en/latest/plugins/authz-keycloak.md index 67618eaaf29f..83ca6764f7ed 100644 --- a/docs/en/latest/plugins/authz-keycloak.md +++ b/docs/en/latest/plugins/authz-keycloak.md @@ -125,10 +125,10 @@ 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 value of `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 token using `Token Endpoint`. -It will also check, if REST method is `POST`. +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. From 8f34241434d4646b3ef369c94579203821f241e8 Mon Sep 17 00:00:00 2001 From: azilentech Date: Thu, 17 Mar 2022 10:30:02 +0530 Subject: [PATCH 09/11] Incorporated review comments: --- apisix/plugins/authz-keycloak.lua | 16 +++++++++------- t/plugin/authz-keycloak.t | 29 +---------------------------- 2 files changed, 10 insertions(+), 35 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 338eeea66204..88203b98b355 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -380,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, @@ -456,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, @@ -644,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", @@ -732,13 +732,13 @@ local function generate_token_using_password_grant(conf,ctx) if not token_endpoint then local err = "Unable to determine token endpoint." log.error(err) - return 500, err + return 503, err end local httpc = authz_keycloak_get_http_client(conf) local params = { method = "POST", - body = ngx.encode_args({ + body = ngx.encode_args({ grant_type = "password", client_id = client_id, client_secret = conf.client_secret, @@ -775,9 +775,11 @@ local function generate_token_using_password_grant(conf,ctx) end function _M.access(conf, ctx) + local headers = core.request.headers(ctx) if conf.password_grant_token_generation_incoming_uri and - ngx.var.request_uri:upper() == - conf.password_grant_token_generation_incoming_uri:upper() and + ngx.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" then return generate_token_using_password_grant(conf,ctx) end diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index 5c4a8298bc64..c98c830598e9 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -652,35 +652,8 @@ Location: http://127.0.0.1/test "type": "roundrobin" }, "uri": "/api/token" - }]], - [[{ - "node": { - "value": { - "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" - }, - "key": "/apisix/routes/1" - }, - "action": "set" }]] - ) + ) if code >= 300 then ngx.status = code From c1d1c8db9d703fabaf4eb1af9426587fc6769841 Mon Sep 17 00:00:00 2001 From: azilentech Date: Fri, 18 Mar 2022 18:06:10 +0530 Subject: [PATCH 10/11] introduced need_grant_token for composite conditions for better readability --- apisix/plugins/authz-keycloak.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 88203b98b355..a117f5cda9d7 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -776,12 +776,12 @@ end function _M.access(conf, ctx) local headers = core.request.headers(ctx) - if conf.password_grant_token_generation_incoming_uri and - ngx.var.request_uri == - conf.password_grant_token_generation_incoming_uri and + local need_grant_token = conf.password_grant_token_generation_incoming_uri and + ngx.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" then - return generate_token_using_password_grant(conf,ctx) + 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) From bd911388b810ad7807ead69ff6e2a88340b1f37e Mon Sep 17 00:00:00 2001 From: azilentech Date: Sun, 20 Mar 2022 21:43:51 +0530 Subject: [PATCH 11/11] used ctx.var instead of ngx.var --- apisix/plugins/authz-keycloak.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index a117f5cda9d7..25902e157ea1 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -777,7 +777,7 @@ end function _M.access(conf, ctx) local headers = core.request.headers(ctx) local need_grant_token = conf.password_grant_token_generation_incoming_uri and - ngx.var.request_uri == 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