Files
2026-07-13 12:32:21 +08:00

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