1171 lines
36 KiB
Lua
1171 lines
36 KiB
Lua
local url = require "socket.url"
|
|
local constants = require "kong.constants"
|
|
local timestamp = require "kong.tools.timestamp"
|
|
local secret = require "kong.plugins.oauth2.secret"
|
|
|
|
|
|
local sha256_base64url = require "kong.tools.sha256".sha256_base64url
|
|
|
|
local fmt = string.format
|
|
local kong = kong
|
|
local type = type
|
|
local next = next
|
|
local table = table
|
|
local error = error
|
|
local split_once = require("kong.tools.string").split_once
|
|
local strip = require("kong.tools.string").strip
|
|
local string_find = string.find
|
|
local string_gsub = string.gsub
|
|
local string_byte = string.byte
|
|
local check_https = require("kong.tools.http").check_https
|
|
local encode_args = require("kong.tools.http").encode_args
|
|
local random_string = require("kong.tools.rand").random_string
|
|
local table_contains = require("kong.tools.table").table_contains
|
|
|
|
|
|
local ngx_decode_args = ngx.decode_args
|
|
local ngx_re_gmatch = ngx.re.gmatch
|
|
local ngx_decode_base64 = ngx.decode_base64
|
|
local ngx_encode_base64 = ngx.encode_base64
|
|
|
|
|
|
local _M = {}
|
|
|
|
|
|
local EMPTY = require("kong.tools.table").EMPTY
|
|
local SLASH = string_byte("/")
|
|
local RESPONSE_TYPE = "response_type"
|
|
local STATE = "state"
|
|
local CODE = "code"
|
|
local CODE_CHALLENGE = "code_challenge"
|
|
local CODE_CHALLENGE_METHOD = "code_challenge_method"
|
|
local CODE_VERIFIER = "code_verifier"
|
|
local CLIENT_TYPE_PUBLIC = "public"
|
|
local CLIENT_TYPE_CONFIDENTIAL = "confidential"
|
|
local TOKEN = "token"
|
|
local REFRESH_TOKEN = "refresh_token"
|
|
local SCOPE = "scope"
|
|
local CLIENT_ID = "client_id"
|
|
local CLIENT_SECRET = "client_secret"
|
|
local REDIRECT_URI = "redirect_uri"
|
|
local ACCESS_TOKEN = "access_token"
|
|
local GRANT_TYPE = "grant_type"
|
|
local GRANT_AUTHORIZATION_CODE = "authorization_code"
|
|
local GRANT_CLIENT_CREDENTIALS = "client_credentials"
|
|
local GRANT_REFRESH_TOKEN = "refresh_token"
|
|
local GRANT_PASSWORD = "password"
|
|
local ERROR = "error"
|
|
local AUTHENTICATED_USERID = "authenticated_userid"
|
|
|
|
|
|
local base64url_encode
|
|
local base64url_decode
|
|
do
|
|
local BASE64URL_ENCODE_CHARS = "[+/]"
|
|
local BASE64URL_ENCODE_SUBST = {
|
|
["+"] = "-",
|
|
["/"] = "_",
|
|
}
|
|
|
|
base64url_encode = function(value)
|
|
value = ngx_encode_base64(value, true)
|
|
if not value then
|
|
return nil
|
|
end
|
|
|
|
return string_gsub(value, BASE64URL_ENCODE_CHARS, BASE64URL_ENCODE_SUBST)
|
|
end
|
|
|
|
|
|
local BASE64URL_DECODE_CHARS = "[-_]"
|
|
local BASE64URL_DECODE_SUBST = {
|
|
["-"] = "+",
|
|
["_"] = "/",
|
|
}
|
|
|
|
base64url_decode = function(value)
|
|
value = string_gsub(value, BASE64URL_DECODE_CHARS, BASE64URL_DECODE_SUBST)
|
|
return ngx_decode_base64(value)
|
|
end
|
|
end
|
|
|
|
-- Helper function to construct WWW-Authenticate header
|
|
local make_www_authenticate_header
|
|
do
|
|
local table_insert = table.insert
|
|
local table_concat = table.concat
|
|
local table_isempty = require("table.isempty")
|
|
|
|
make_www_authenticate_header = function(realm, error_code, error_description)
|
|
local params = {}
|
|
|
|
if realm and realm ~= "" then
|
|
table_insert(params, fmt('realm="%s"', realm))
|
|
end
|
|
|
|
if error_code then
|
|
table_insert(params, fmt('error="%s"', error_code))
|
|
end
|
|
|
|
if error_description then
|
|
table_insert(params, fmt('error_description="%s"', error_description))
|
|
end
|
|
|
|
if not table_isempty(params) then
|
|
return "Bearer " .. table_concat(params, ", ")
|
|
else
|
|
return "Bearer"
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
local function generate_token(conf, service, credential, authenticated_userid,
|
|
scope, state, disable_refresh, existing_token)
|
|
|
|
local token_expiration = conf.token_expiration
|
|
|
|
local refresh_token_ttl
|
|
if conf.refresh_token_ttl and conf.refresh_token_ttl > 0 then
|
|
refresh_token_ttl = conf.refresh_token_ttl
|
|
end
|
|
|
|
local service_id
|
|
if not conf.global_credentials then
|
|
service_id = service.id
|
|
end
|
|
|
|
local refresh_token
|
|
local token, err
|
|
if existing_token and conf.reuse_refresh_token then
|
|
token, err = kong.db.oauth2_tokens:update(existing_token, {
|
|
access_token = random_string(),
|
|
expires_in = token_expiration,
|
|
created_at = timestamp.get_utc() / 1000
|
|
}, {
|
|
-- Access tokens (and their associated refresh token) are being
|
|
-- permanently deleted after 'refresh_token_ttl' seconds
|
|
ttl = token_expiration > 0 and refresh_token_ttl or nil
|
|
})
|
|
refresh_token = token.refresh_token -- required for output
|
|
else
|
|
if not disable_refresh and token_expiration > 0 then
|
|
refresh_token = random_string()
|
|
end
|
|
token, err = kong.db.oauth2_tokens:insert({
|
|
service = service_id and { id = service_id } or nil,
|
|
credential = { id = credential.id },
|
|
authenticated_userid = authenticated_userid,
|
|
expires_in = token_expiration,
|
|
refresh_token = refresh_token,
|
|
scope = scope
|
|
}, {
|
|
-- Access tokens (and their associated refresh token) are being
|
|
-- permanently deleted after 'refresh_token_ttl' seconds
|
|
ttl = token_expiration > 0 and refresh_token_ttl or nil
|
|
})
|
|
end
|
|
|
|
if err then
|
|
return error(err)
|
|
end
|
|
|
|
return {
|
|
access_token = token.access_token,
|
|
token_type = "bearer",
|
|
expires_in = token_expiration > 0 and token.expires_in or nil,
|
|
refresh_token = refresh_token,
|
|
state = state -- If state is nil, this value won't be added
|
|
}
|
|
end
|
|
|
|
|
|
local function load_oauth2_credential_by_client_id(client_id)
|
|
local credential, err = kong.db.oauth2_credentials:select_by_client_id(client_id)
|
|
if err then
|
|
return nil, err
|
|
end
|
|
|
|
return credential
|
|
end
|
|
|
|
|
|
local function get_redirect_uris(client_id)
|
|
local client, err
|
|
if type(client_id) == "string" and client_id ~= "" then
|
|
local credential_cache_key = kong.db.oauth2_credentials:cache_key(client_id)
|
|
client, err = kong.cache:get(credential_cache_key, nil,
|
|
load_oauth2_credential_by_client_id,
|
|
client_id)
|
|
if err then
|
|
return error(err)
|
|
end
|
|
end
|
|
|
|
return client and client.redirect_uris or nil, client
|
|
end
|
|
|
|
|
|
local function retrieve_parameters()
|
|
-- OAuth2 parameters could be in both the querystring or body
|
|
local uri_args = kong.request.get_query()
|
|
local method = kong.request.get_method()
|
|
|
|
if method == "POST" or method == "PUT" or method == "PATCH" then
|
|
local body_args = kong.request.get_body()
|
|
|
|
return kong.table.merge(uri_args, body_args)
|
|
end
|
|
|
|
return uri_args
|
|
end
|
|
|
|
|
|
local function retrieve_scope(parameters, conf)
|
|
local scope = parameters[SCOPE]
|
|
local scopes = {}
|
|
|
|
if conf.scopes and scope ~= nil then
|
|
if type(scope) ~= "string" then
|
|
return nil, {[ERROR] = "invalid_scope", error_description = "scope must be a string"}
|
|
end
|
|
|
|
for v in scope:gmatch("%S+") do
|
|
if not table_contains(conf.scopes, v) then
|
|
return nil, {[ERROR] = "invalid_scope", error_description = "\"" .. v .. "\" is an invalid " .. SCOPE}
|
|
else
|
|
table.insert(scopes, v)
|
|
end
|
|
end
|
|
|
|
elseif not scope and conf.mandatory_scope then
|
|
return nil, {[ERROR] = "invalid_scope", error_description = "You must specify a " .. SCOPE}
|
|
end
|
|
|
|
if #scopes > 0 then
|
|
return table.concat(scopes, " ")
|
|
end -- else return nil
|
|
end
|
|
|
|
local function retrieve_code_challenge(parameters)
|
|
local challenge = parameters[CODE_CHALLENGE]
|
|
local challenge_method = parameters[CODE_CHALLENGE_METHOD]
|
|
|
|
if challenge_method and not challenge then
|
|
return nil, CODE_CHALLENGE .. " is required when code_method is present"
|
|
end
|
|
|
|
if challenge then
|
|
local challenge_decoded = base64url_decode(challenge)
|
|
if challenge_decoded then
|
|
challenge = base64url_encode(challenge_decoded)
|
|
end
|
|
|
|
if challenge_method and challenge_method ~= "S256" then
|
|
if challenge_method ~= "s256" then
|
|
return nil, CODE_CHALLENGE_METHOD .. " is not supported, must be S256"
|
|
end
|
|
|
|
challenge_method = "S256"
|
|
end
|
|
end
|
|
|
|
return challenge, nil, challenge_method or "S256"
|
|
end
|
|
|
|
local function requires_pkce(conf, client, used_pkce)
|
|
if not client then
|
|
return false
|
|
end
|
|
|
|
if used_pkce -- only set on token endpoint
|
|
or (client.client_type == CLIENT_TYPE_PUBLIC and conf.pkce ~= "none")
|
|
or (client.client_type == CLIENT_TYPE_CONFIDENTIAL and conf.pkce == "strict")
|
|
then
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
local function authorize(conf)
|
|
local response_params = {}
|
|
local parameters = retrieve_parameters()
|
|
local state = parameters[STATE]
|
|
local allowed_redirect_uris, client, redirect_uri, parsed_redirect_uri
|
|
local is_implicit_grant
|
|
|
|
local is_https, err = check_https(kong.ip.is_trusted(kong.client.get_ip()),
|
|
conf.accept_http_if_already_terminated)
|
|
if not is_https then
|
|
response_params = {
|
|
[ERROR] = "access_denied",
|
|
error_description = err or "You must use HTTPS"
|
|
}
|
|
|
|
else
|
|
if conf.provision_key ~= parameters.provision_key then
|
|
response_params = {
|
|
[ERROR] = "invalid_provision_key",
|
|
error_description = "Invalid provision_key"
|
|
}
|
|
|
|
elseif not parameters.authenticated_userid or strip(parameters.authenticated_userid) == "" then
|
|
response_params = {
|
|
[ERROR] = "invalid_authenticated_userid",
|
|
error_description = "Missing authenticated_userid parameter"
|
|
}
|
|
|
|
else
|
|
local response_type = parameters[RESPONSE_TYPE]
|
|
|
|
-- Check response_type
|
|
if not ((response_type == CODE and conf.enable_authorization_code) or
|
|
(conf.enable_implicit_grant and response_type == TOKEN)) then
|
|
-- Auth Code Grant (http://tools.ietf.org/html/rfc6749#section-4.1.1)
|
|
response_params = {
|
|
[ERROR] = "unsupported_response_type",
|
|
error_description = "Invalid " .. RESPONSE_TYPE
|
|
}
|
|
end
|
|
|
|
-- Check scopes
|
|
local scopes, err = retrieve_scope(parameters, conf)
|
|
if err then
|
|
response_params = err -- If it's not ok, then this is the error message
|
|
end
|
|
|
|
-- Check client_id and redirect_uri
|
|
allowed_redirect_uris, client = get_redirect_uris(parameters[CLIENT_ID])
|
|
|
|
if not allowed_redirect_uris then
|
|
response_params = {
|
|
[ERROR] = "invalid_client",
|
|
error_description = "Invalid client authentication"
|
|
}
|
|
|
|
else
|
|
redirect_uri = parameters[REDIRECT_URI] and
|
|
parameters[REDIRECT_URI] or
|
|
allowed_redirect_uris[1]
|
|
|
|
if not table_contains(allowed_redirect_uris, redirect_uri) then
|
|
response_params = {
|
|
[ERROR] = "invalid_request",
|
|
error_description = "Invalid " .. REDIRECT_URI ..
|
|
" that does not match with any redirect_uri" ..
|
|
" created with the application"
|
|
}
|
|
|
|
-- redirect_uri used in this case is the first one registered with
|
|
-- the application
|
|
redirect_uri = allowed_redirect_uris[1]
|
|
end
|
|
end
|
|
|
|
parsed_redirect_uri = url.parse(redirect_uri)
|
|
|
|
local challenge, err, challenge_method = retrieve_code_challenge(parameters)
|
|
if err then
|
|
response_params = {
|
|
[ERROR] = "invalid_request",
|
|
error_description = err
|
|
}
|
|
elseif client and not challenge and requires_pkce(conf, client) then
|
|
response_params = {
|
|
[ERROR] = "invalid_request",
|
|
error_description = CODE_CHALLENGE .. " is required for " .. client.client_type .. " clients"
|
|
}
|
|
elseif not challenge then -- do not save a code method unless we have a challenge
|
|
challenge_method = nil
|
|
end
|
|
|
|
-- If there are no errors, keep processing the request
|
|
if not response_params[ERROR] then
|
|
if response_type == CODE then
|
|
local service_id
|
|
if not conf.global_credentials then
|
|
service_id = (kong.router.get_service() or EMPTY).id
|
|
end
|
|
|
|
local auth_code, err = kong.db.oauth2_authorization_codes:insert({
|
|
service = service_id and { id = service_id } or nil,
|
|
credential = { id = client.id },
|
|
authenticated_userid = parameters[AUTHENTICATED_USERID],
|
|
scope = scopes,
|
|
challenge = challenge,
|
|
challenge_method = challenge_method,
|
|
plugin = { id = kong.plugin.get_id() },
|
|
}, {
|
|
ttl = 300
|
|
})
|
|
|
|
if err then
|
|
error(err)
|
|
end
|
|
|
|
response_params = {
|
|
code = auth_code.code,
|
|
}
|
|
|
|
else
|
|
-- Implicit grant, override expiration to zero
|
|
response_params = generate_token(conf, kong.router.get_service(),
|
|
client,
|
|
parameters[AUTHENTICATED_USERID],
|
|
scopes, state, true)
|
|
is_implicit_grant = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Adding the state if it exists. If the state == nil then it won't be added
|
|
response_params.state = state
|
|
|
|
-- Appending kong generated params to redirect_uri query string
|
|
if parsed_redirect_uri then
|
|
local encoded_params = encode_args(kong.table.merge(ngx_decode_args(
|
|
(is_implicit_grant and
|
|
(parsed_redirect_uri.fragment and parsed_redirect_uri.fragment or "") or
|
|
(parsed_redirect_uri.query and parsed_redirect_uri.query or "")
|
|
)), response_params))
|
|
|
|
if is_implicit_grant then
|
|
parsed_redirect_uri.fragment = encoded_params
|
|
else
|
|
parsed_redirect_uri.query = encoded_params
|
|
end
|
|
end
|
|
|
|
-- Sending response in JSON format
|
|
local status = response_params[ERROR] and 400 or 200
|
|
local body
|
|
if redirect_uri then
|
|
body = { redirect_uri = url.build(parsed_redirect_uri) }
|
|
|
|
else
|
|
body = response_params
|
|
end
|
|
|
|
return kong.response.exit(status, body, {
|
|
["cache-control"] = "no-store",
|
|
["pragma"] = "no-cache"
|
|
})
|
|
end
|
|
|
|
|
|
local function retrieve_client_credentials(parameters, conf)
|
|
local client_id, client_secret, from_authorization_header
|
|
local authorization_header = kong.request.get_header(conf.auth_header_name)
|
|
|
|
if parameters[CLIENT_ID] and parameters[CLIENT_SECRET] then
|
|
client_id = parameters[CLIENT_ID]
|
|
client_secret = parameters[CLIENT_SECRET]
|
|
|
|
elseif authorization_header then
|
|
from_authorization_header = true
|
|
|
|
local iterator, iter_err = ngx_re_gmatch(authorization_header,
|
|
"\\s*[Bb]asic\\s*(.+)",
|
|
"jo")
|
|
if not iterator then
|
|
kong.log.err(iter_err)
|
|
return
|
|
end
|
|
|
|
local m, err = iterator()
|
|
if err then
|
|
kong.log.err(err)
|
|
return
|
|
end
|
|
|
|
if m and next(m) then
|
|
local decoded_basic = ngx_decode_base64(m[1])
|
|
if decoded_basic then
|
|
client_id, client_secret = split_once(decoded_basic, ":")
|
|
end
|
|
end
|
|
|
|
elseif parameters[CLIENT_ID] then
|
|
client_id = parameters[CLIENT_ID]
|
|
end
|
|
|
|
return client_id, client_secret, from_authorization_header
|
|
end
|
|
|
|
local function validate_pkce_verifier(parameters, auth_code)
|
|
local verifier = parameters[CODE_VERIFIER]
|
|
if not verifier then
|
|
return {
|
|
[ERROR] = "invalid_request",
|
|
error_description = CODE_VERIFIER .. " is required for PKCE authorization requests",
|
|
}
|
|
elseif type(verifier) ~= "string" then
|
|
return {
|
|
[ERROR] = "invalid_request",
|
|
error_description = CODE_VERIFIER .. " is not a string",
|
|
}
|
|
end
|
|
|
|
if #verifier < 43 or #verifier > 128 then
|
|
return {
|
|
[ERROR] = "invalid_request",
|
|
error_description = CODE_VERIFIER .. " must be between 43 and 128 characters",
|
|
}
|
|
end
|
|
|
|
local challenge = sha256_base64url(verifier)
|
|
|
|
if not challenge
|
|
or not auth_code.challenge
|
|
or challenge ~= auth_code.challenge
|
|
then
|
|
return {
|
|
[ERROR] = "invalid_grant",
|
|
error_description = "Invalid " .. CODE
|
|
}
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
local function issue_token(conf)
|
|
local response_params = {}
|
|
local invalid_client_properties = {}
|
|
|
|
local parameters = retrieve_parameters()
|
|
local state = parameters[STATE]
|
|
|
|
local is_https, err = check_https(kong.ip.is_trusted(kong.client.get_ip()),
|
|
conf.accept_http_if_already_terminated)
|
|
if not is_https then
|
|
response_params = {
|
|
[ERROR] = "access_denied",
|
|
error_description = err or "You must use HTTPS"
|
|
}
|
|
|
|
else
|
|
local grant_type = parameters[GRANT_TYPE]
|
|
if not (grant_type == GRANT_AUTHORIZATION_CODE or
|
|
grant_type == GRANT_REFRESH_TOKEN or
|
|
(conf.enable_client_credentials and
|
|
grant_type == GRANT_CLIENT_CREDENTIALS) or
|
|
(conf.enable_password_grant and grant_type == GRANT_PASSWORD)) then
|
|
response_params = {
|
|
[ERROR] = "unsupported_grant_type",
|
|
error_description = "Invalid " .. GRANT_TYPE
|
|
}
|
|
end
|
|
|
|
local client_id, client_secret, from_authorization_header =
|
|
retrieve_client_credentials(parameters, conf)
|
|
|
|
-- Check client_id and redirect_uri
|
|
local allowed_redirect_uris, client = get_redirect_uris(client_id)
|
|
if grant_type ~= GRANT_CLIENT_CREDENTIALS then
|
|
if allowed_redirect_uris then
|
|
local redirect_uri = parameters[REDIRECT_URI] and
|
|
parameters[REDIRECT_URI] or
|
|
allowed_redirect_uris[1]
|
|
|
|
if not table_contains(allowed_redirect_uris, redirect_uri) then
|
|
response_params = {
|
|
[ERROR] = "invalid_request",
|
|
error_description = "Invalid " .. REDIRECT_URI .. " that does " ..
|
|
"not match with any redirect_uri created " ..
|
|
"with the application"
|
|
}
|
|
end
|
|
|
|
else
|
|
response_params = {
|
|
[ERROR] = "invalid_client",
|
|
error_description = "Invalid client authentication"
|
|
}
|
|
|
|
if from_authorization_header then
|
|
invalid_client_properties = {
|
|
status = 401,
|
|
www_authenticate = "Basic realm=\"OAuth2.0\""
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
if client then
|
|
if client.client_type == CLIENT_TYPE_CONFIDENTIAL then
|
|
local authenticated
|
|
if client.hash_secret then
|
|
authenticated = secret.verify(client_secret, client.client_secret)
|
|
if authenticated and secret.needs_rehash(client.client_secret) then
|
|
local pk = kong.db.oauth2_credentials.schema:extract_pk_values(client)
|
|
local ok, err = kong.db.oauth2_credentials:update(pk, {
|
|
client_secret = client_secret,
|
|
hash_secret = true,
|
|
})
|
|
|
|
if not ok then
|
|
kong.log.warn(err)
|
|
end
|
|
end
|
|
|
|
else
|
|
authenticated = client.client_secret == client_secret
|
|
end
|
|
|
|
if not authenticated then
|
|
response_params = {
|
|
[ERROR] = "invalid_client",
|
|
error_description = "Invalid client authentication"
|
|
}
|
|
|
|
if from_authorization_header then
|
|
invalid_client_properties = {
|
|
status = 401,
|
|
www_authenticate = "Basic realm=\"OAuth2.0\""
|
|
}
|
|
end
|
|
end
|
|
|
|
elseif client.client_type == CLIENT_TYPE_PUBLIC and strip(client_secret) ~= "" then
|
|
response_params = {
|
|
[ERROR] = "invalid_request",
|
|
error_description = "client_secret is disallowed for " .. CLIENT_TYPE_PUBLIC .. " clients"
|
|
}
|
|
end
|
|
end
|
|
|
|
if not response_params[ERROR] then
|
|
if grant_type == GRANT_AUTHORIZATION_CODE then
|
|
local code = parameters[CODE]
|
|
|
|
local service_id
|
|
if not conf.global_credentials then
|
|
service_id = (kong.router.get_service() or EMPTY).id
|
|
end
|
|
|
|
local auth_code =
|
|
code and kong.db.oauth2_authorization_codes:select_by_code(code)
|
|
if not auth_code or (service_id and service_id ~= auth_code.service.id) then
|
|
response_params = {
|
|
[ERROR] = "invalid_request",
|
|
error_description = "Invalid " .. CODE
|
|
}
|
|
elseif auth_code.credential.id ~= client.id then
|
|
response_params = {
|
|
[ERROR] = "invalid_request",
|
|
error_description = "Invalid " .. CODE
|
|
}
|
|
end
|
|
|
|
-- if the code was generated by a PKCE request, then check the code verifier
|
|
local client_requires_pkce = auth_code and requires_pkce(conf, client, auth_code.challenge_method)
|
|
if not response_params[ERROR] and client_requires_pkce then
|
|
local err = validate_pkce_verifier(parameters, auth_code)
|
|
if err then
|
|
response_params = err
|
|
end
|
|
end
|
|
|
|
if not response_params[ERROR] and conf.global_credentials then
|
|
-- verify only if plugin is present to avoid existing codes being fails
|
|
if auth_code.plugin and
|
|
(kong.plugin.get_id() ~= auth_code.plugin.id) then
|
|
response_params = {
|
|
[ERROR] = "invalid_request",
|
|
error_description = "Invalid " .. CODE
|
|
}
|
|
end
|
|
end
|
|
|
|
if not response_params[ERROR] then
|
|
if not auth_code or (service_id and service_id ~= auth_code.service.id)
|
|
then
|
|
response_params = {
|
|
[ERROR] = "invalid_request",
|
|
error_description = "Invalid " .. CODE
|
|
}
|
|
|
|
elseif auth_code.credential.id ~= client.id then
|
|
response_params = {
|
|
[ERROR] = "invalid_request",
|
|
error_description = "Invalid " .. CODE
|
|
}
|
|
|
|
else
|
|
response_params = generate_token(conf, kong.router.get_service(),
|
|
client,
|
|
auth_code.authenticated_userid,
|
|
auth_code.scope, state)
|
|
|
|
-- Delete authorization code so it cannot be reused
|
|
kong.db.oauth2_authorization_codes:delete(auth_code)
|
|
end
|
|
end
|
|
|
|
elseif grant_type == GRANT_CLIENT_CREDENTIALS then
|
|
-- Only check the provision_key if the authenticated_userid is being set
|
|
if parameters.authenticated_userid and
|
|
conf.provision_key ~= parameters.provision_key then
|
|
response_params = {
|
|
[ERROR] = "invalid_provision_key",
|
|
error_description = "Invalid provision_key"
|
|
}
|
|
|
|
elseif not client then
|
|
response_params = {
|
|
[ERROR] = "invalid_client",
|
|
error_description = "Invalid client authentication"
|
|
}
|
|
|
|
else
|
|
-- Check scopes
|
|
local scope, err = retrieve_scope(parameters, conf)
|
|
if err then
|
|
-- If it's not ok, then this is the error message
|
|
response_params = err
|
|
|
|
else
|
|
response_params = generate_token(conf, kong.router.get_service(),
|
|
client,
|
|
parameters.authenticated_userid,
|
|
scope, state, true)
|
|
end
|
|
end
|
|
|
|
elseif grant_type == GRANT_PASSWORD then
|
|
-- Check that it comes from the right client
|
|
if conf.provision_key ~= parameters.provision_key then
|
|
response_params = {
|
|
[ERROR] = "invalid_provision_key",
|
|
error_description = "Invalid provision_key"
|
|
}
|
|
|
|
elseif not parameters.authenticated_userid or
|
|
strip(parameters.authenticated_userid) == "" then
|
|
response_params = {
|
|
[ERROR] = "invalid_authenticated_userid",
|
|
error_description = "Missing authenticated_userid parameter"
|
|
}
|
|
|
|
else
|
|
-- Check scopes
|
|
local scope, err = retrieve_scope(parameters, conf)
|
|
if err then
|
|
-- If it's not ok, then this is the error message
|
|
response_params = err
|
|
|
|
else
|
|
response_params = generate_token(conf, kong.router.get_service(),
|
|
client,
|
|
parameters.authenticated_userid,
|
|
scope, state)
|
|
end
|
|
end
|
|
|
|
elseif grant_type == GRANT_REFRESH_TOKEN then
|
|
local refresh_token = parameters[REFRESH_TOKEN]
|
|
|
|
local service_id
|
|
if not conf.global_credentials then
|
|
service_id = (kong.router.get_service() or EMPTY).id
|
|
end
|
|
|
|
local token = refresh_token and
|
|
kong.db.oauth2_tokens:select_by_refresh_token(refresh_token)
|
|
|
|
if not token or (service_id and service_id ~= token.service.id) then
|
|
response_params = {
|
|
[ERROR] = "invalid_request",
|
|
error_description = "Invalid " .. REFRESH_TOKEN
|
|
}
|
|
|
|
-- Check that the token belongs to the client application
|
|
elseif token.credential.id ~= client.id then
|
|
response_params = {
|
|
[ERROR] = "invalid_client",
|
|
error_description = "Invalid client authentication"
|
|
}
|
|
|
|
else
|
|
-- Check scopes
|
|
if token.scope then
|
|
for scope in token.scope:gmatch("%S+") do
|
|
if not table_contains(conf.scopes, scope) then
|
|
response_params = {
|
|
[ERROR] = "invalid_scope",
|
|
error_description = "Scope mismatch",
|
|
}
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if not response_params[ERROR] then
|
|
response_params = generate_token(conf, kong.router.get_service(),
|
|
client,
|
|
token.authenticated_userid,
|
|
token.scope, state, false, token)
|
|
-- Delete old token if refresh token not persisted
|
|
if not conf.reuse_refresh_token then
|
|
kong.db.oauth2_tokens:delete(token)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Adding the state if it exists. If the state == nil then it won't be added
|
|
response_params.state = state
|
|
|
|
-- Sending response in JSON format
|
|
return kong.response.exit(response_params[ERROR] and
|
|
(invalid_client_properties and
|
|
invalid_client_properties.status or 400) or 200,
|
|
response_params, {
|
|
["cache-control"] = "no-store",
|
|
["pragma"] = "no-cache",
|
|
["www-authenticate"] = invalid_client_properties and
|
|
invalid_client_properties.www_authenticate
|
|
}
|
|
)
|
|
end
|
|
|
|
|
|
local function load_token(access_token)
|
|
return kong.db.oauth2_tokens:select_by_access_token(access_token)
|
|
end
|
|
|
|
|
|
local function retrieve_token(conf, access_token)
|
|
local token_cache_key = kong.db.oauth2_tokens:cache_key(access_token)
|
|
local token, err = kong.cache:get(token_cache_key, nil, load_token, access_token)
|
|
if err then
|
|
return error(err)
|
|
end
|
|
if not token then
|
|
return
|
|
end
|
|
|
|
if not conf.global_credentials then
|
|
if not token.service then
|
|
return kong.response.exit(401, {
|
|
[ERROR] = "invalid_token",
|
|
error_description = "The access token is global, but the current " ..
|
|
"plugin is configured without 'global_credentials'",
|
|
},
|
|
{
|
|
["WWW-Authenticate"] = make_www_authenticate_header(conf.realm, "invalid_token",
|
|
"The access token is invalid or has expired"),
|
|
})
|
|
end
|
|
|
|
if token.service.id ~= kong.router.get_service().id then
|
|
return nil
|
|
end
|
|
end
|
|
|
|
return token
|
|
end
|
|
|
|
|
|
local function parse_access_token(conf)
|
|
local found_in = {}
|
|
|
|
local access_token = kong.request.get_header(conf.auth_header_name)
|
|
if access_token then
|
|
local parts = {}
|
|
for v in access_token:gmatch("%S+") do -- Split by space
|
|
table.insert(parts, v)
|
|
end
|
|
|
|
if #parts == 2 and (parts[1]:lower() == "token" or
|
|
parts[1]:lower() == "bearer") then
|
|
access_token = parts[2]
|
|
found_in.authorization_header = true
|
|
end
|
|
|
|
else
|
|
access_token = retrieve_parameters()[ACCESS_TOKEN]
|
|
if type(access_token) ~= "string" then
|
|
return
|
|
end
|
|
end
|
|
|
|
if conf.hide_credentials then
|
|
if found_in.authorization_header then
|
|
kong.service.request.clear_header(conf.auth_header_name)
|
|
|
|
else
|
|
-- Remove from querystring
|
|
local parameters = kong.request.get_query()
|
|
parameters[ACCESS_TOKEN] = nil
|
|
kong.service.request.set_query(parameters)
|
|
|
|
local content_type = kong.request.get_header("content-type")
|
|
local is_form_post = content_type and
|
|
string_find(content_type, "application/x-www-form-urlencoded", 1, true)
|
|
|
|
if kong.request.get_method() ~= "GET" and is_form_post then
|
|
-- Remove from body
|
|
parameters = kong.request.get_body() or {}
|
|
parameters[ACCESS_TOKEN] = nil
|
|
kong.service.request.set_body(parameters)
|
|
end
|
|
end
|
|
end
|
|
|
|
return access_token
|
|
end
|
|
|
|
|
|
local function load_oauth2_credential_into_memory(credential_id)
|
|
local result, err = kong.db.oauth2_credentials:select({ id = credential_id })
|
|
if err then
|
|
return nil, err
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
|
|
local function set_consumer(consumer, credential, token)
|
|
kong.client.authenticate(consumer, credential)
|
|
|
|
local set_header = kong.service.request.set_header
|
|
local clear_header = kong.service.request.clear_header
|
|
|
|
if consumer and consumer.id then
|
|
set_header(constants.HEADERS.CONSUMER_ID, consumer.id)
|
|
else
|
|
clear_header(constants.HEADERS.CONSUMER_ID)
|
|
end
|
|
|
|
if consumer and consumer.custom_id then
|
|
set_header(constants.HEADERS.CONSUMER_CUSTOM_ID, consumer.custom_id)
|
|
else
|
|
clear_header(constants.HEADERS.CONSUMER_CUSTOM_ID)
|
|
end
|
|
|
|
if consumer and consumer.username then
|
|
set_header(constants.HEADERS.CONSUMER_USERNAME, consumer.username)
|
|
else
|
|
clear_header(constants.HEADERS.CONSUMER_USERNAME)
|
|
end
|
|
|
|
if credential and credential.client_id then
|
|
set_header(constants.HEADERS.CREDENTIAL_IDENTIFIER, credential.client_id)
|
|
else
|
|
clear_header(constants.HEADERS.CREDENTIAL_IDENTIFIER)
|
|
end
|
|
|
|
if credential then
|
|
clear_header(constants.HEADERS.ANONYMOUS)
|
|
else
|
|
set_header(constants.HEADERS.ANONYMOUS, true)
|
|
end
|
|
|
|
if token and token.scope then
|
|
set_header("X-Authenticated-Scope", token.scope)
|
|
else
|
|
clear_header("X-Authenticated-Scope")
|
|
end
|
|
|
|
if token and token.authenticated_userid then
|
|
set_header("X-Authenticated-UserId", token.authenticated_userid)
|
|
else
|
|
clear_header("X-Authenticated-UserId")
|
|
end
|
|
end
|
|
|
|
|
|
local function do_authentication(conf)
|
|
local access_token = parse_access_token(conf)
|
|
if not access_token or access_token == "" then
|
|
return nil, {
|
|
status = 401,
|
|
message = {
|
|
[ERROR] = "invalid_request",
|
|
error_description = "The access token is missing"
|
|
},
|
|
headers = {
|
|
["WWW-Authenticate"] = make_www_authenticate_header(conf.realm)
|
|
}
|
|
}
|
|
end
|
|
|
|
local token = retrieve_token(conf, access_token)
|
|
if not token then
|
|
return nil, {
|
|
status = 401,
|
|
message = {
|
|
[ERROR] = "invalid_token",
|
|
error_description = "The access token is invalid or has expired"
|
|
},
|
|
headers = {
|
|
["WWW-Authenticate"] = make_www_authenticate_header(conf.realm, "invalid_token",
|
|
"The access token is invalid or has expired")
|
|
}
|
|
}
|
|
end
|
|
|
|
if (token.service and token.service.id and
|
|
kong.router.get_service().id ~= token.service.id) or
|
|
((not token.service or not token.service.id) and
|
|
not conf.global_credentials) then
|
|
return nil, {
|
|
status = 401,
|
|
message = {
|
|
[ERROR] = "invalid_token",
|
|
error_description = "The access token is invalid or has expired"
|
|
},
|
|
headers = {
|
|
["WWW-Authenticate"] = make_www_authenticate_header(conf.realm, "invalid_token",
|
|
"The access token is invalid or has expired")
|
|
}
|
|
}
|
|
end
|
|
|
|
-- Check expiration date
|
|
if token.expires_in > 0 then -- zero means the token never expires
|
|
local now = timestamp.get_utc() / 1000
|
|
if now - token.created_at > token.expires_in then
|
|
return nil, {
|
|
status = 401,
|
|
message = {
|
|
[ERROR] = "invalid_token",
|
|
error_description = "The access token is invalid or has expired"
|
|
},
|
|
headers = {
|
|
["WWW-Authenticate"] = make_www_authenticate_header(conf.realm, "invalid_token",
|
|
"The access token is invalid or has expired")
|
|
}
|
|
}
|
|
end
|
|
end
|
|
|
|
-- Retrieve the credential from the token
|
|
local credential_cache_key =
|
|
kong.db.oauth2_credentials:cache_key(token.credential.id)
|
|
|
|
local credential, err = kong.cache:get(credential_cache_key, nil,
|
|
load_oauth2_credential_into_memory,
|
|
token.credential.id)
|
|
if err then
|
|
return error(err)
|
|
end
|
|
|
|
-- Retrieve the consumer from the credential
|
|
local consumer_cache_key, consumer
|
|
consumer_cache_key = kong.db.consumers:cache_key(credential.consumer.id)
|
|
consumer, err = kong.cache:get(consumer_cache_key, nil,
|
|
kong.client.load_consumer,
|
|
credential.consumer.id)
|
|
if err then
|
|
return error(err)
|
|
end
|
|
|
|
set_consumer(consumer, credential, token)
|
|
|
|
return true
|
|
end
|
|
|
|
local function invalid_oauth2_method(endpoint_name, realm)
|
|
local error_description = "The HTTP method "
|
|
.. kong.request.get_method()
|
|
.. " is invalid for the "
|
|
.. endpoint_name
|
|
.. " endpoint"
|
|
|
|
return {
|
|
status = 405,
|
|
message = {
|
|
[ERROR] = "invalid_method",
|
|
error_description = error_description
|
|
},
|
|
headers = {
|
|
["WWW-Authenticate"] = make_www_authenticate_header(realm, "invalid_method", error_description)
|
|
}
|
|
}
|
|
end
|
|
|
|
local function set_anonymous_consumer(anonymous)
|
|
local consumer_cache_key = kong.db.consumers:cache_key(anonymous)
|
|
local consumer, err = kong.cache:get(consumer_cache_key, nil,
|
|
kong.client.load_consumer,
|
|
anonymous, true)
|
|
if err then
|
|
return error(err)
|
|
end
|
|
|
|
if not consumer then
|
|
local err_msg = "anonymous consumer " .. anonymous .. " is configured but doesn't exist"
|
|
kong.log.err(err_msg)
|
|
return kong.response.error(500, err_msg)
|
|
end
|
|
|
|
set_consumer(consumer)
|
|
end
|
|
|
|
--- When conf.anonymous is enabled we are in "logical OR" authentication flow.
|
|
--- Meaning - either anonymous consumer is enabled or there are multiple auth plugins
|
|
--- and we need to passthrough on failed authentication.
|
|
local function logical_OR_authentication(conf)
|
|
if kong.client.get_credential() then
|
|
-- we're already authenticated and in "logical OR" between auth methods -- early exit
|
|
local clear_header = kong.service.request.clear_header
|
|
clear_header("X-Authenticated-Scope")
|
|
clear_header("X-Authenticated-UserId")
|
|
return
|
|
end
|
|
|
|
local ok, _ = do_authentication(conf)
|
|
if not ok then
|
|
set_anonymous_consumer(conf.anonymous)
|
|
end
|
|
end
|
|
|
|
--- When conf.anonymous is not set we are in "logical AND" authentication flow.
|
|
--- Meaning - if this authentication fails the request should not be authorized
|
|
--- even though other auth plugins might have successfully authorized user.
|
|
local function logical_AND_authentication(conf)
|
|
local ok, err = do_authentication(conf)
|
|
if not ok then
|
|
return kong.response.exit(err.status, err.message, err.headers)
|
|
end
|
|
end
|
|
|
|
function _M.execute(conf)
|
|
local path = kong.request.get_path()
|
|
local has_end_slash = string_byte(path, -1) == SLASH
|
|
|
|
if string_find(path, "/oauth2/token", has_end_slash and -14 or -13, true) then
|
|
if kong.request.get_method() ~= "POST" then
|
|
local err = invalid_oauth2_method("token", conf.realm)
|
|
return kong.response.exit(err.status, err.message, err.headers)
|
|
end
|
|
|
|
return issue_token(conf)
|
|
end
|
|
|
|
if string_find(path, "/oauth2/authorize", has_end_slash and -18 or -17, true) then
|
|
if kong.request.get_method() ~= "POST" then
|
|
local err = invalid_oauth2_method("authorization", conf.realm)
|
|
return kong.response.exit(err.status, err.message, err.headers)
|
|
end
|
|
|
|
return authorize(conf)
|
|
end
|
|
|
|
if conf.anonymous then
|
|
return logical_OR_authentication(conf)
|
|
else
|
|
return logical_AND_authentication(conf)
|
|
end
|
|
end
|
|
|
|
|
|
return _M
|