- Implements Apache APISIX packaging for Cloudron platform. - Includes Dockerfile, CloudronManifest.json, and start.sh. - Configured to use Cloudron's etcd addon. 🤖 Generated with Gemini CLI Co-Authored-By: Gemini <noreply@google.com>
791 lines
26 KiB
Lua
791 lines
26 KiB
Lua
--
|
|
-- 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 type = type
|
|
local ngx = ngx
|
|
local plugin_name = "authz-keycloak"
|
|
local fetch_secrets = require("apisix.secret").fetch_secrets
|
|
|
|
local log = core.log
|
|
local pairs = pairs
|
|
|
|
local schema = {
|
|
type = "object",
|
|
properties = {
|
|
discovery = {type = "string", minLength = 1, maxLength = 4096},
|
|
token_endpoint = {type = "string", minLength = 1, maxLength = 4096},
|
|
resource_registration_endpoint = {type = "string", minLength = 1, maxLength = 4096},
|
|
client_id = {type = "string", minLength = 1, maxLength = 100},
|
|
client_secret = {type = "string", minLength = 1, maxLength = 100},
|
|
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
|
|
},
|
|
policy_enforcement_mode = {
|
|
type = "string",
|
|
enum = {"ENFORCING", "PERMISSIVE"},
|
|
default = "ENFORCING"
|
|
},
|
|
permissions = {
|
|
type = "array",
|
|
items = {
|
|
type = "string",
|
|
minLength = 1, maxLength = 100
|
|
},
|
|
uniqueItems = true,
|
|
default = {}
|
|
},
|
|
lazy_load_paths = {type = "boolean", default = false},
|
|
http_method_as_scope = {type = "boolean", default = false},
|
|
timeout = {type = "integer", minimum = 1000, default = 3000},
|
|
ssl_verify = {type = "boolean", default = true},
|
|
cache_ttl_seconds = {type = "integer", minimum = 1, default = 24 * 60 * 60},
|
|
keepalive = {type = "boolean", default = true},
|
|
keepalive_timeout = {type = "integer", minimum = 1000, default = 60000},
|
|
keepalive_pool = {type = "integer", minimum = 1, default = 5},
|
|
access_denied_redirect_uri = {type = "string", minLength = 1, maxLength = 2048},
|
|
access_token_expires_in = {type = "integer", minimum = 1, default = 300},
|
|
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
|
|
},
|
|
},
|
|
encrypt_fields = {"client_secret"},
|
|
required = {"client_id"},
|
|
allOf = {
|
|
-- Require discovery or token endpoint.
|
|
{
|
|
anyOf = {
|
|
{required = {"discovery"}},
|
|
{required = {"token_endpoint"}}
|
|
}
|
|
},
|
|
-- If lazy_load_paths is true, require discovery or resource registration endpoint.
|
|
{
|
|
anyOf = {
|
|
{
|
|
properties = {
|
|
lazy_load_paths = {enum = {false}},
|
|
}
|
|
},
|
|
{
|
|
properties = {
|
|
lazy_load_paths = {enum = {true}},
|
|
},
|
|
anyOf = {
|
|
{required = {"discovery"}},
|
|
{required = {"resource_registration_endpoint"}}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
local _M = {
|
|
version = 0.1,
|
|
priority = 2000,
|
|
name = plugin_name,
|
|
schema = schema,
|
|
}
|
|
|
|
|
|
function _M.check_schema(conf)
|
|
local check = {"discovery", "token_endpoint", "resource_registration_endpoint",
|
|
"access_denied_redirect_uri"}
|
|
core.utils.check_https(check, conf, plugin_name)
|
|
core.utils.check_tls_bool({"ssl_verify"}, conf, plugin_name)
|
|
|
|
return core.schema.check(schema, conf)
|
|
end
|
|
|
|
|
|
-- Some auxiliary functions below heavily inspired by the excellent
|
|
-- lua-resty-openidc module; see https://github.com/zmartzone/lua-resty-openidc
|
|
|
|
|
|
-- Retrieve value from server-wide cache, if available.
|
|
local function authz_keycloak_cache_get(type, key)
|
|
local dict = ngx.shared[type]
|
|
local value
|
|
if dict then
|
|
value = dict:get(key)
|
|
if value then log.debug("cache hit: type=", type, " key=", key) end
|
|
end
|
|
return value
|
|
end
|
|
|
|
|
|
-- Set value in server-wide cache, if available.
|
|
local function authz_keycloak_cache_set(type, key, value, exp)
|
|
local dict = ngx.shared[type]
|
|
if dict and (exp > 0) then
|
|
local success, err, forcible = dict:set(key, value, exp)
|
|
if err then
|
|
log.error("cache set: success=", success, " err=", err, " forcible=", forcible)
|
|
else
|
|
log.debug("cache set: success=", success, " err=", err, " forcible=", forcible)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
-- Configure request parameters.
|
|
local function authz_keycloak_configure_params(params, conf)
|
|
-- Keepalive options.
|
|
if conf.keepalive then
|
|
params.keepalive_timeout = conf.keepalive_timeout
|
|
params.keepalive_pool = conf.keepalive_pool
|
|
else
|
|
params.keepalive = conf.keepalive
|
|
end
|
|
|
|
-- TLS verification.
|
|
params.ssl_verify = conf.ssl_verify
|
|
|
|
-- Decorate parameters, maybe, and return.
|
|
return conf.http_request_decorator and conf.http_request_decorator(params) or params
|
|
end
|
|
|
|
|
|
-- Configure timeouts.
|
|
local function authz_keycloak_configure_timeouts(httpc, timeout)
|
|
if timeout then
|
|
if type(timeout) == "table" then
|
|
httpc:set_timeouts(timeout.connect or 0, timeout.send or 0, timeout.read or 0)
|
|
else
|
|
httpc:set_timeout(timeout)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
-- Set outgoing proxy options.
|
|
local function authz_keycloak_configure_proxy(httpc, proxy_opts)
|
|
if httpc and proxy_opts and type(proxy_opts) == "table" then
|
|
log.debug("authz_keycloak_configure_proxy : use http proxy")
|
|
httpc:set_proxy_options(proxy_opts)
|
|
else
|
|
log.debug("authz_keycloak_configure_proxy : don't use http proxy")
|
|
end
|
|
end
|
|
|
|
|
|
-- Get and configure HTTP client.
|
|
local function authz_keycloak_get_http_client(conf)
|
|
local httpc = http.new()
|
|
authz_keycloak_configure_timeouts(httpc, conf.timeout)
|
|
authz_keycloak_configure_proxy(httpc, conf.proxy_opts)
|
|
return httpc
|
|
end
|
|
|
|
|
|
-- Parse the JSON result from a call to the OP.
|
|
local function authz_keycloak_parse_json_response(response)
|
|
local err
|
|
local res
|
|
|
|
-- Check the response from the OP.
|
|
if response.status ~= 200 then
|
|
err = "response indicates failure, status=" .. response.status .. ", body=" .. response.body
|
|
else
|
|
-- Decode the response and extract the JSON object.
|
|
res, err = core.json.decode(response.body)
|
|
|
|
if not res then
|
|
err = "JSON decoding failed: " .. err
|
|
end
|
|
end
|
|
|
|
return res, err
|
|
end
|
|
|
|
|
|
-- Get the Discovery metadata from the specified URL.
|
|
local function authz_keycloak_discover(conf)
|
|
log.debug("authz_keycloak_discover: URL is: " .. conf.discovery)
|
|
|
|
local json, err
|
|
local v = authz_keycloak_cache_get("discovery", conf.discovery)
|
|
|
|
if not v then
|
|
log.debug("Discovery data not in cache, making call to discovery endpoint.")
|
|
|
|
-- Make the call to the discovery endpoint.
|
|
local httpc = authz_keycloak_get_http_client(conf)
|
|
|
|
local params = authz_keycloak_configure_params({}, conf)
|
|
|
|
local res, error = httpc:request_uri(conf.discovery, params)
|
|
|
|
if not res then
|
|
err = "Accessing discovery URL (" .. conf.discovery .. ") failed: " .. error
|
|
log.error(err)
|
|
else
|
|
log.debug("Response data: " .. res.body)
|
|
json, err = authz_keycloak_parse_json_response(res)
|
|
if json then
|
|
authz_keycloak_cache_set("discovery", conf.discovery, core.json.encode(json),
|
|
conf.cache_ttl_seconds)
|
|
else
|
|
err = "could not decode JSON from Discovery data" .. (err and (": " .. err) or '')
|
|
log.error(err)
|
|
end
|
|
end
|
|
else
|
|
json = core.json.decode(v)
|
|
end
|
|
|
|
return json, err
|
|
end
|
|
|
|
|
|
-- Turn a discovery url set in the conf dictionary into the discovered information.
|
|
local function authz_keycloak_ensure_discovered_data(conf)
|
|
local err
|
|
if type(conf.discovery) == "string" then
|
|
local discovery
|
|
discovery, err = authz_keycloak_discover(conf)
|
|
if not err then
|
|
conf.discovery = discovery
|
|
end
|
|
end
|
|
return err
|
|
end
|
|
|
|
|
|
-- Get an endpoint from the configuration.
|
|
local function authz_keycloak_get_endpoint(conf, endpoint)
|
|
if conf and conf[endpoint] then
|
|
-- Use explicit entry.
|
|
return conf[endpoint]
|
|
elseif conf and conf.discovery and type(conf.discovery) == "table" then
|
|
-- Use discovery data.
|
|
return conf.discovery[endpoint]
|
|
end
|
|
|
|
-- Unable to obtain endpoint.
|
|
return nil
|
|
end
|
|
|
|
|
|
-- Return the token endpoint from the configuration.
|
|
local function authz_keycloak_get_token_endpoint(conf)
|
|
return authz_keycloak_get_endpoint(conf, "token_endpoint")
|
|
end
|
|
|
|
|
|
-- Return the resource registration endpoint from the configuration.
|
|
local function authz_keycloak_get_resource_registration_endpoint(conf)
|
|
return authz_keycloak_get_endpoint(conf, "resource_registration_endpoint")
|
|
end
|
|
|
|
|
|
-- Return access_token expires_in value (in seconds).
|
|
local function authz_keycloak_access_token_expires_in(conf, expires_in)
|
|
return (expires_in or conf.access_token_expires_in)
|
|
- 1 - conf.access_token_expires_leeway
|
|
end
|
|
|
|
|
|
-- Return refresh_token expires_in value (in seconds).
|
|
local function authz_keycloak_refresh_token_expires_in(conf, expires_in)
|
|
return (expires_in or conf.refresh_token_expires_in)
|
|
- 1 - conf.refresh_token_expires_leeway
|
|
end
|
|
|
|
|
|
-- Ensure a valid service account access token is available for the configured client.
|
|
local function authz_keycloak_ensure_sa_access_token(conf)
|
|
local client_id = conf.client_id
|
|
local ttl = conf.cache_ttl_seconds
|
|
local token_endpoint = authz_keycloak_get_token_endpoint(conf)
|
|
|
|
if not token_endpoint then
|
|
log.error("Unable to determine token endpoint.")
|
|
return 503, "Unable to determine token endpoint."
|
|
end
|
|
|
|
local session = authz_keycloak_cache_get("access-tokens", token_endpoint .. ":"
|
|
.. client_id)
|
|
|
|
if session then
|
|
-- Decode session string.
|
|
local err
|
|
session, err = core.json.decode(session)
|
|
|
|
if not session then
|
|
-- Should never happen.
|
|
return 500, err
|
|
end
|
|
|
|
local current_time = ngx.time()
|
|
|
|
if current_time < session.access_token_expiration then
|
|
-- Access token is still valid.
|
|
log.debug("Access token is still valid.")
|
|
return session.access_token
|
|
else
|
|
-- Access token has expired.
|
|
log.debug("Access token has expired.")
|
|
if session.refresh_token
|
|
and (not session.refresh_token_expiration
|
|
or current_time < session.refresh_token_expiration) then
|
|
-- Try to get a new access token, using the refresh token.
|
|
log.debug("Trying to get new access token using refresh token.")
|
|
|
|
local httpc = authz_keycloak_get_http_client(conf)
|
|
|
|
local params = {
|
|
method = "POST",
|
|
body = ngx.encode_args({
|
|
grant_type = "refresh_token",
|
|
client_id = client_id,
|
|
client_secret = conf.client_secret,
|
|
refresh_token = session.refresh_token,
|
|
}),
|
|
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 nil, 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 token endpoint"
|
|
.. (err and (": " .. err) or '.')
|
|
log.error(err)
|
|
return nil, err
|
|
end
|
|
|
|
if not json.access_token then
|
|
-- Clear session.
|
|
log.debug("Answer didn't contain a new access token. Clearing session.")
|
|
session = nil
|
|
else
|
|
log.debug("Got new access token.")
|
|
-- Save access token.
|
|
session.access_token = json.access_token
|
|
|
|
-- Calculate and save access token expiry time.
|
|
session.access_token_expiration = current_time
|
|
+ authz_keycloak_access_token_expires_in(conf, json.expires_in)
|
|
|
|
-- Save refresh token, maybe.
|
|
if json.refresh_token ~= nil then
|
|
log.debug("Got new refresh token.")
|
|
session.refresh_token = json.refresh_token
|
|
|
|
-- Calculate and save refresh token expiry time.
|
|
session.refresh_token_expiration = current_time
|
|
+ authz_keycloak_refresh_token_expires_in(conf,
|
|
json.refresh_expires_in)
|
|
end
|
|
|
|
authz_keycloak_cache_set("access-tokens",
|
|
token_endpoint .. ":" .. client_id,
|
|
core.json.encode(session), ttl)
|
|
end
|
|
else
|
|
-- No refresh token available, or it has expired. Clear session.
|
|
log.debug("No or expired refresh token. Clearing session.")
|
|
session = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
if not session then
|
|
-- No session available. Create a new one.
|
|
|
|
log.debug("Getting access token for Protection API from token endpoint.")
|
|
local httpc = authz_keycloak_get_http_client(conf)
|
|
|
|
local params = {
|
|
method = "POST",
|
|
body = ngx.encode_args({
|
|
grant_type = "client_credentials",
|
|
client_id = client_id,
|
|
client_secret = conf.client_secret,
|
|
}),
|
|
headers = {
|
|
["Content-Type"] = "application/x-www-form-urlencoded"
|
|
}
|
|
}
|
|
|
|
params = authz_keycloak_configure_params(params, conf)
|
|
|
|
local current_time = ngx.time()
|
|
|
|
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 nil, 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 token endpoint" .. (err and (": " .. err) or '.')
|
|
log.error(err)
|
|
return nil, err
|
|
end
|
|
|
|
if not json.access_token then
|
|
err = "Response does not contain access_token field."
|
|
log.error(err)
|
|
return nil, err
|
|
end
|
|
|
|
session = {}
|
|
|
|
-- Save access token.
|
|
session.access_token = json.access_token
|
|
|
|
-- Calculate and save access token expiry time.
|
|
session.access_token_expiration = current_time
|
|
+ authz_keycloak_access_token_expires_in(conf, json.expires_in)
|
|
|
|
-- Save refresh token, maybe.
|
|
if json.refresh_token ~= nil then
|
|
session.refresh_token = json.refresh_token
|
|
|
|
-- Calculate and save refresh token expiry time.
|
|
session.refresh_token_expiration = current_time
|
|
+ authz_keycloak_refresh_token_expires_in(conf, json.refresh_expires_in)
|
|
end
|
|
|
|
authz_keycloak_cache_set("access-tokens", token_endpoint .. ":" .. client_id,
|
|
core.json.encode(session), ttl)
|
|
end
|
|
|
|
return session.access_token
|
|
end
|
|
|
|
|
|
-- Resolve a URI to one or more resource IDs.
|
|
local function authz_keycloak_resolve_resource(conf, uri, sa_access_token)
|
|
-- Get resource registration endpoint URL.
|
|
local resource_registration_endpoint = authz_keycloak_get_resource_registration_endpoint(conf)
|
|
|
|
if not resource_registration_endpoint then
|
|
local err = "Unable to determine registration endpoint."
|
|
log.error(err)
|
|
return nil, err
|
|
end
|
|
|
|
log.debug("Resource registration endpoint: ", resource_registration_endpoint)
|
|
|
|
local httpc = authz_keycloak_get_http_client(conf)
|
|
|
|
local params = {
|
|
method = "GET",
|
|
query = {uri = uri, matchingUri = "true"},
|
|
headers = {
|
|
["Authorization"] = "Bearer " .. sa_access_token
|
|
}
|
|
}
|
|
|
|
params = authz_keycloak_configure_params(params, conf)
|
|
|
|
local res, err = httpc:request_uri(resource_registration_endpoint, params)
|
|
|
|
if not res then
|
|
err = "Accessing resource registration endpoint URL (" .. resource_registration_endpoint
|
|
.. ") failed: " .. err
|
|
log.error(err)
|
|
return nil, err
|
|
end
|
|
|
|
log.debug("Response data: " .. res.body)
|
|
res.body = '{"resources": ' .. res.body .. '}'
|
|
local json, err = authz_keycloak_parse_json_response(res)
|
|
|
|
if not json then
|
|
err = "Could not decode JSON from resource registration endpoint"
|
|
.. (err and (": " .. err) or '.')
|
|
log.error(err)
|
|
return nil, err
|
|
end
|
|
|
|
return json.resources
|
|
end
|
|
|
|
|
|
local function evaluate_permissions(conf, ctx, token)
|
|
-- Ensure discovered data.
|
|
local err = authz_keycloak_ensure_discovered_data(conf)
|
|
if err then
|
|
return 503, err
|
|
end
|
|
|
|
local permission
|
|
|
|
if conf.lazy_load_paths then
|
|
-- Ensure service account access token.
|
|
local sa_access_token, err = authz_keycloak_ensure_sa_access_token(conf)
|
|
if err then
|
|
log.error(err)
|
|
return 503, err
|
|
end
|
|
|
|
-- Resolve URI to resource(s).
|
|
permission, err = authz_keycloak_resolve_resource(conf, ctx.var.request_uri,
|
|
sa_access_token)
|
|
|
|
-- Check result.
|
|
if permission == nil then
|
|
-- No result back from resource registration endpoint.
|
|
log.error(err)
|
|
return 503, err
|
|
end
|
|
else
|
|
-- Use statically configured permissions.
|
|
permission = conf.permissions
|
|
end
|
|
|
|
-- Return 403 or 307 if permission is empty and enforcement mode is "ENFORCING".
|
|
if #permission == 0 and conf.policy_enforcement_mode == "ENFORCING" then
|
|
-- Return Keycloak-style message for consistency.
|
|
if conf.access_denied_redirect_uri then
|
|
core.response.set_header("Location", conf.access_denied_redirect_uri)
|
|
return 307
|
|
end
|
|
return 403, '{"error":"access_denied","error_description":"not_authorized"}'
|
|
end
|
|
|
|
-- Determine scope from HTTP method, maybe.
|
|
local scope
|
|
if conf.http_method_as_scope then
|
|
scope = ctx.var.request_method
|
|
end
|
|
|
|
if scope then
|
|
-- Loop over permissions and add scope.
|
|
for k, v in pairs(permission) do
|
|
if v:find("#", 1, true) then
|
|
-- Already contains scope.
|
|
permission[k] = v .. ", " .. scope
|
|
else
|
|
-- Doesn't contain scope yet.
|
|
permission[k] = v .. "#" .. scope
|
|
end
|
|
end
|
|
end
|
|
|
|
for k, v in pairs(permission) do
|
|
log.debug("Requesting permission ", v, ".")
|
|
end
|
|
|
|
-- Get token endpoint URL.
|
|
local token_endpoint = authz_keycloak_get_token_endpoint(conf)
|
|
if not token_endpoint then
|
|
err = "Unable to determine token endpoint."
|
|
log.error(err)
|
|
return 503, err
|
|
end
|
|
log.debug("Token endpoint: ", token_endpoint)
|
|
|
|
local httpc = authz_keycloak_get_http_client(conf)
|
|
|
|
local params = {
|
|
method = "POST",
|
|
body = ngx.encode_args({
|
|
grant_type = conf.grant_type,
|
|
audience = conf.client_id,
|
|
response_mode = "decision",
|
|
permission = permission
|
|
}),
|
|
headers = {
|
|
["Content-Type"] = "application/x-www-form-urlencoded",
|
|
["Authorization"] = token
|
|
}
|
|
}
|
|
|
|
params = authz_keycloak_configure_params(params, conf)
|
|
|
|
local res, err = httpc:request_uri(token_endpoint, params)
|
|
|
|
if not res then
|
|
err = "Error while sending authz request to " .. token_endpoint .. ": " .. err
|
|
log.error(err)
|
|
return 503
|
|
end
|
|
|
|
log.debug("Response status: ", res.status, ", data: ", res.body)
|
|
|
|
if res.status == 403 then
|
|
-- Request permanently denied, e.g. due to lacking permissions.
|
|
log.debug('Request denied: HTTP 403 Forbidden. Body: ', res.body)
|
|
if conf.access_denied_redirect_uri then
|
|
core.response.set_header("Location", conf.access_denied_redirect_uri)
|
|
return 307
|
|
end
|
|
|
|
return res.status, res.body
|
|
elseif res.status == 401 then
|
|
-- Request temporarily denied, e.g access token not valid.
|
|
log.debug('Request denied: HTTP 401 Unauthorized. Body: ', res.body)
|
|
return res.status, res.body
|
|
elseif res.status >= 400 then
|
|
-- Some other error. Log full response.
|
|
log.error('Request denied: Token endpoint returned an error (status: ',
|
|
res.status, ', body: ', res.body, ').')
|
|
return res.status, res.body
|
|
end
|
|
|
|
-- Request accepted.
|
|
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
|
|
|
|
-- 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 = core.string.decode_args(body)
|
|
|
|
local username = parameters["username"]
|
|
local password = parameters["password"]
|
|
|
|
if not username then
|
|
local err = "username is missing."
|
|
log.warn(err)
|
|
return 422, {message = err}
|
|
end
|
|
if not password then
|
|
local err = "password is missing."
|
|
log.warn(err)
|
|
return 422, {message = err}
|
|
end
|
|
|
|
local client_id = conf.client_id
|
|
|
|
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, {message = 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 = "Accessing token endpoint URL failed."}
|
|
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 = "Could not decode JSON from response."}
|
|
end
|
|
|
|
return res.status, res.body
|
|
end
|
|
|
|
function _M.access(conf, ctx)
|
|
-- resolve secrets
|
|
conf = fetch_secrets(conf, true, conf, "")
|
|
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
|
|
log.error("failed to fetch JWT token: ", err)
|
|
return 401, {message = "Missing JWT token in request"}
|
|
end
|
|
|
|
local status, body = evaluate_permissions(conf, ctx, jwt_token)
|
|
if status then
|
|
return status, body
|
|
end
|
|
end
|
|
|
|
|
|
return _M
|