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