kong/spec/03-plugins/25-oauth2/03-access_spec.lua (3,814 lines of code) (raw):

local cjson = require "cjson" local helpers = require "spec.helpers" local utils = require "kong.tools.utils" local admin_api = require "spec.fixtures.admin_api" local sha256 = require "resty.sha256" local jwt_encoder = require "kong.plugins.jwt.jwt_parser" local math_random = math.random local string_char = string.char local string_gsub = string.gsub local string_rep = string.rep local ngx_encode_base64 = ngx.encode_base64 local PAYLOAD = { iss = nil, nbf = os.time(), iat = os.time(), exp = os.time() + 3600 } local kong = { table = require("kong.pdk.table").new() } local function provision_code(host, extra_headers, client_id, code_challenge) local request_client = helpers.proxy_ssl_client() local body = { provision_key = "provision123", client_id = client_id or "clientid123", scope = "email", response_type = "code", state = "hello", authenticated_userid = "userid123", } if code_challenge then body["code_challenge"] = code_challenge body["code_method"] = "S256" end local res = assert(request_client:send { method = "POST", path = "/oauth2/authorize", body = body, headers = kong.table.merge({ ["Host"] = host or "oauth2.com", ["Content-Type"] = "application/json" }, extra_headers) }) assert.response(res).has.status(200) local body = assert.response(res).has.jsonbody() request_client:close() if body.redirect_uri then local iterator, err = ngx.re.gmatch(body.redirect_uri, "^http://google\\.com/kong\\?code=([\\w]{32,32})&state=hello$") assert.is_nil(err) local m, err = iterator() assert.is_nil(err) return m[1] end end local function provision_token(host, extra_headers, client_id, client_secret, code_challenge, code_verifier, require_secret) local code = provision_code(host, extra_headers, client_id, code_challenge) local request_client = helpers.proxy_ssl_client() require_secret = require_secret == nil or require_secret local body = { code = code, client_id = client_id or "clientid123", grant_type = "authorization_code" } if client_secret or require_secret then body["client_secret"] = client_secret or "secret123" end if code_verifier then body["code_verifier"] = code_verifier end local res = assert(request_client:send { method = "POST", path = "/oauth2/token", body = body, headers = kong.table.merge({ ["Host"] = host or "oauth2.com", ["Content-Type"] = "application/json" }, extra_headers) }) assert.response(res).has.status(200) local token = assert.response(res).has.jsonbody() assert.is_table(token) request_client:close() return token end local function refresh_token(host, refresh_token) local request_client = helpers.proxy_ssl_client() local res = assert(request_client:send { method = "POST", path = "/oauth2/token", body = { refresh_token = refresh_token, client_id = "clientid123", client_secret = "secret123", grant_type = "refresh_token" }, headers = { ["Host"] = host or "oauth2.com", ["Content-Type"] = "application/json" } }) assert.response(res).has.status(200) local token = assert.response(res).has.jsonbody() assert.is_table(token) request_client:close() return token end local function get_pkce_tokens(code_verifier) if not code_verifier then code_verifier = '' for i = 1, 50 do code_verifier = code_verifier .. string_char(math_random(65, 90)) end end local digest = sha256:new() digest:update(code_verifier) local code_challenge = ngx_encode_base64(digest:final(), true) code_challenge = string_gsub(code_challenge, "+", "-") code_challenge = string_gsub(code_challenge, "/", "_") return code_challenge, code_verifier end for _, strategy in helpers.each_strategy() do describe("Plugin: oauth2 [#" .. strategy .. "]", function() local db lazy_setup(function() local _ _, db = helpers.get_db_utils(strategy, { "routes", "services", "consumers", "plugins", "keyauth_credentials", "jwt_secrets", "oauth2_credentials", "oauth2_authorization_codes", "oauth2_tokens", }) assert(helpers.start_kong({ database = strategy, trusted_ips = "127.0.0.1", nginx_conf = "spec/fixtures/custom_nginx.template", })) end) lazy_teardown(function() helpers.stop_kong() end) describe("access", function() local proxy_ssl_client local proxy_client local client1 lazy_setup(function() local consumer = admin_api.consumers:insert { username = "bob" } local anonymous_user = admin_api.consumers:insert { username = "no-body" } client1 = admin_api.oauth2_credentials:insert { client_id = "clientid123", client_secret = "secret123", hash_secret = true, redirect_uris = { "http://google.com/kong" }, name = "testapp", consumer = { id = consumer.id }, } admin_api.oauth2_credentials:insert { client_id = "clientid789", client_secret = "secret789", redirect_uris = { "http://google.com/kong?foo=bar&code=123" }, name = "testapp2", consumer = { id = consumer.id }, } admin_api.oauth2_credentials:insert { client_id = "clientid333", client_secret = "secret333", hash_secret = true, redirect_uris = { "http://google.com/kong" }, name = "testapp3", consumer = { id = consumer.id }, } admin_api.oauth2_credentials:insert { client_id = "clientid456", client_secret = "secret456", redirect_uris = { "http://one.com/one/", "http://two.com/two" }, name = "testapp3", consumer = { id = consumer.id }, } admin_api.oauth2_credentials:insert { client_id = "clientid1011", client_secret = "secret1011", hash_secret = true, redirect_uris = { "http://google.com/kong", }, name = "testapp31", consumer = { id = consumer.id }, } admin_api.oauth2_credentials:insert { client_id = "clientid10112", client_secret = "secret10112", redirect_uris = ngx.null, name = "testapp311", consumer = { id = consumer.id }, } admin_api.oauth2_credentials:insert { client_id = "clientid11211", client_secret = "secret11211", redirect_uris = { "http://google.com/kong", }, name = "testapp50", client_type = "public", consumer = { id = consumer.id }, } local service1 = admin_api.services:insert() local service2 = admin_api.services:insert() local service2bis = admin_api.services:insert() local service3 = admin_api.services:insert() local service4 = admin_api.services:insert() local service5 = admin_api.services:insert() local service6 = admin_api.services:insert() local service7 = admin_api.services:insert() local service8 = admin_api.services:insert() local service9 = admin_api.services:insert() local service10 = admin_api.services:insert() local service11 = admin_api.services:insert() local service12 = admin_api.services:insert() local service13 = admin_api.services:insert() local service_c = admin_api.services:insert() local service14 = admin_api.services:insert() local service15 = admin_api.services:insert() local service16 = admin_api.services:insert() local route1 = assert(admin_api.routes:insert({ hosts = { "oauth2.com" }, protocols = { "http", "https" }, service = service1, })) local route2 = assert(admin_api.routes:insert({ hosts = { "example-path.com" }, protocols = { "http", "https" }, service = service2, })) local route2bis = assert(admin_api.routes:insert({ paths = { "/somepath" }, protocols = { "http", "https" }, service = service2bis, })) local route3 = assert(admin_api.routes:insert({ hosts = { "oauth2_3.com" }, protocols = { "http", "https" }, service = service3, })) local route4 = assert(admin_api.routes:insert({ hosts = { "oauth2_4.com" }, protocols = { "http", "https" }, service = service4, })) local route5 = assert(admin_api.routes:insert({ hosts = { "oauth2_5.com" }, protocols = { "http", "https" }, service = service5, })) local route6 = assert(admin_api.routes:insert({ hosts = { "oauth2_6.com" }, protocols = { "http", "https" }, service = service6, })) local route7 = assert(admin_api.routes:insert({ hosts = { "oauth2_7.com" }, protocols = { "http", "https" }, service = service7, })) local route8 = assert(admin_api.routes:insert({ hosts = { "oauth2_8.com" }, protocols = { "http", "https" }, service = service8, })) local route9 = assert(admin_api.routes:insert({ hosts = { "oauth2_9.com" }, protocols = { "http", "https" }, service = service9, })) local route10 = assert(admin_api.routes:insert({ hosts = { "oauth2_10.com" }, protocols = { "http", "https" }, service = service10, })) local route11 = assert(admin_api.routes:insert({ hosts = { "oauth2_11.com" }, protocols = { "http", "https" }, service = service11, })) local route12 = assert(admin_api.routes:insert({ hosts = { "oauth2_12.com" }, protocols = { "http", "https" }, service = service12, })) local route13 = assert(admin_api.routes:insert({ hosts = { "oauth2_13.com" }, protocols = { "http", "https" }, service = service13, })) local route_c = assert(admin_api.routes:insert({ hosts = { "oauth2__c.com" }, protocols = { "http", "https" }, service = service_c, })) local route14 = assert(admin_api.routes:insert({ hosts = { "oauth2_14.com" }, protocols = { "http", "https" }, service = service14, })) local route15 = assert(admin_api.routes:insert({ hosts = { "oauth2_15.com" }, protocols = { "http", "https" }, service = service15, })) local route16 = assert(admin_api.routes:insert({ hosts = { "oauth2_16.com" }, protocols = { "http", "https" }, service = service16, })) local service_grpc = assert(admin_api.services:insert { name = "grpc", url = helpers.grpcbin_url, }) local route_grpc = assert(admin_api.routes:insert { protocols = { "grpc", "grpcs" }, hosts = { "oauth2_grpc.com" }, paths = { "/hello.HelloService/SayHello" }, service = service_grpc, }) local route_provgrpc = assert(admin_api.routes:insert { hosts = { "oauth2_grpc.com" }, paths = { "/" }, service = service_grpc, }) admin_api.oauth2_plugins:insert({ route = { id = route_grpc.id }, config = { scopes = { "email", "profile", "user.email" }, }, }) admin_api.oauth2_plugins:insert({ route = { id = route_provgrpc.id }, config = { scopes = { "email", "profile", "user.email" }, }, }) admin_api.oauth2_plugins:insert({ route = { id = route1.id }, config = { scopes = { "email", "profile", "user.email" } }, }) admin_api.oauth2_plugins:insert({ route = { id = route2.id } }) admin_api.oauth2_plugins:insert({ route = { id = route2bis.id } }) admin_api.oauth2_plugins:insert({ route = { id = route3.id }, config = { hide_credentials = true }, }) admin_api.oauth2_plugins:insert({ route = { id = route4.id }, config = { enable_client_credentials = true, enable_authorization_code = false, }, }) admin_api.oauth2_plugins:insert({ route = { id = route5.id }, config = { enable_password_grant = true, enable_authorization_code = false, }, }) admin_api.oauth2_plugins:insert({ route = { id = route6.id }, config = { scopes = { "email", "profile", "user.email" }, provision_key = "provision123", accept_http_if_already_terminated = true, }, }) admin_api.oauth2_plugins:insert({ route = { id = route7.id }, config = { scopes = { "email", "profile", "user.email" }, anonymous = anonymous_user.id, }, }) admin_api.oauth2_plugins:insert({ route = { id = route8.id }, config = { scopes = { "email", "profile", "user.email" }, global_credentials = true, }, }) admin_api.oauth2_plugins:insert({ route = { id = route9.id }, config = { scopes = { "email", "profile", "user.email" }, global_credentials = true, }, }) admin_api.oauth2_plugins:insert({ route = { id = route10.id }, config = { scopes = { "email", "profile", "user.email" }, global_credentials = true, anonymous = utils.uuid(), -- a non existing consumer }, }) admin_api.oauth2_plugins:insert({ route = { id = route11.id }, config = { scopes = { "email", "profile", "user.email" }, global_credentials = true, token_expiration = 7, auth_header_name = "custom_header_name", }, }) admin_api.oauth2_plugins:insert({ route = { id = route12.id }, config = { scopes = { "email", "profile", "user.email" }, global_credentials = true, auth_header_name = "custom_header_name", hide_credentials = true, }, }) admin_api.oauth2_plugins:insert({ route = { id = route13.id }, config = { scopes = { "email", "profile", "user.email" }, global_credentials = true, reuse_refresh_token = true, }, }) admin_api.oauth2_plugins:insert({ route = { id = route_c.id }, config = { scopes = { "email", "profile", "user.email" }, anonymous = anonymous_user.username, }, }) admin_api.oauth2_plugins:insert({ route = { id = route14.id }, config = { scopes = { "email", "profile", "user.email" }, global_credentials = true, pkce = "none", }, }) admin_api.oauth2_plugins:insert({ route = { id = route15.id }, config = { scopes = { "email", "profile", "user.email" }, global_credentials = true, pkce = "strict", } }) admin_api.oauth2_plugins:insert({ route = { id = route16.id }, config = { scopes = { "email", "profile", "user.email" }, global_credentials = true, pkce = "lax", } }) end) before_each(function () proxy_client = helpers.proxy_client() proxy_ssl_client = helpers.proxy_ssl_client() end) after_each(function() if proxy_client then proxy_client:close() end if proxy_ssl_client then proxy_ssl_client:close() end end) describe("OAuth2 Authorization", function() describe("Code Grant", function() it("returns an error when no provision_key is being sent", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", headers = { ["Host"] = "oauth2.com" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid provision_key", error = "invalid_provision_key" }, json) assert.are.equal("no-store", res.headers["cache-control"]) assert.are.equal("no-cache", res.headers["pragma"]) end) it("rejects gRPC call without credentials", function() local ok, err = helpers.proxy_client_grpcs(){ service = "hello.HelloService.SayHello", opts = { ["-authority"] = "oauth2.com", }, } assert.falsy(ok) assert.match("Code: Unauthenticated", err) end) it("returns an error when no parameter is being sent", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Missing authenticated_userid parameter", error = "invalid_authenticated_userid" }, json) end) it("returns an error when only provision_key and authenticated_userid are sent", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", authenticated_userid = "id123" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json) assert.are.equal("no-store", res.headers["cache-control"]) assert.are.equal("no-cache", res.headers["pragma"]) end) it("returns an error when only the client_id is being sent", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ redirect_uri = "http://google.com/kong?error=invalid_scope&error_description=You%20must%20specify%20a%20scope" }, json) end) it("returns an error when an invalid scope is being sent", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "wot" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ redirect_uri = "http://google.com/kong?error=invalid_scope&error_description=%22wot%22%20is%20an%20invalid%20scope" }, json) end) it("returns an error when no response_type is being sent", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ redirect_uri = "http://google.com/kong?error=unsupported_response_type&error_description=Invalid%20response_type" }, json) end) it("returns an error with a state when no response_type is being sent", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", state = "somestate" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ redirect_uri = "http://google.com/kong?error=unsupported_response_type&error_description=Invalid%20response_type&state=somestate" }, json) end) it("returns error when the redirect_uri does not match", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "code", redirect_uri = "http://hello.com/" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ redirect_uri = "http://google.com/kong?error=invalid_request&error_description=Invalid%20redirect_uri%20that%20does%20not%20match%20with%20any%20redirect_uri%20created%20with%20the%20application" }, json) end) it("works even if redirect_uri contains a query string", function() local res = assert(proxy_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid789", scope = "email", response_type = "code" }, headers = { ["Host"] = "oauth2_6.com", ["Content-Type"] = "application/json", ["X-Forwarded-Proto"] = "https" } }) local body = cjson.decode(assert.res_status(200, res)) assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}&foo=bar$")) end) it("works with multiple redirect_uris in the application", function() local res = assert(proxy_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid456", scope = "email", response_type = "code" }, headers = { ["Host"] = "oauth2_6.com", ["Content-Type"] = "application/json", ["X-Forwarded-Proto"] = "https" } }) assert.response(res).has.status(200) local json = assert.response(res).has.jsonbody() assert.truthy(ngx.re.match(json.redirect_uri, "^http://one\\.com/one/\\?code=[\\w]{32,32}$")) end) it("fails when not under HTTPS", function() local res = assert(proxy_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "code" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) assert.response(res).has.status(400) local json = assert.response(res).has.jsonbody(res) assert.equal("You must use HTTPS", json.error_description) assert.equal("access_denied", json.error) end) it("works when not under HTTPS but accept_http_if_already_terminated is true", function() local res = assert(proxy_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "code" }, headers = { ["Host"] = "oauth2_6.com", ["Content-Type"] = "application/json", ["X-Forwarded-Proto"] = "https" } }) local body = cjson.decode(assert.res_status(200, res)) assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}$")) end) it("fails when not under HTTPS and accept_http_if_already_terminated is false", function() local res = assert(proxy_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "code" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json", ["X-Forwarded-Proto"] = "https" } }) assert.response(res).has.status(400) local json = assert.response(res).has.jsonbody(res) assert.equal("You must use HTTPS", json.error_description) assert.equal("access_denied", json.error) end) it("returns success", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "code" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = cjson.decode(assert.res_status(200, res)) assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}$")) end) it("fails with a path when using the DNS", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123a", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "code", }, headers = { ["Host"] = "example-path.com", ["Content-Type"] = "application/json", }, }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid provision_key", error = "invalid_provision_key" }, json) end) it("returns success with a path", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/somepath/oauth2/authorize", body = { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "code" }, headers = { ["Content-Type"] = "application/json" } }) local body = cjson.decode(assert.res_status(200, res)) assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}$")) end) it("returns success when requesting the url with final slash", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize/", body = { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "code" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = cjson.decode(assert.res_status(200, res)) assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}$")) end) it("returns success with a state", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "code", state = "hello" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = cjson.decode(assert.res_status(200, res)) assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}&state=hello$")) -- Checking headers assert.are.equal("no-store", res.headers["cache-control"]) assert.are.equal("no-cache", res.headers["pragma"]) end) it("returns success and store authenticated user properties", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", client_id = "clientid123", scope = "email", response_type = "code", state = "hello", authenticated_userid = "userid123" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = cjson.decode(assert.res_status(200, res)) assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}&state=hello$")) local iterator, err = ngx.re.gmatch(body.redirect_uri, "^http://google\\.com/kong\\?code=([\\w]{32,32})&state=hello$") assert.is_nil(err) local m, err = iterator() assert.is_nil(err) local data = db.oauth2_authorization_codes:select_by_code(m[1]) assert.are.equal(m[1], data.code) assert.are.equal("userid123", data.authenticated_userid) assert.are.equal("email", data.scope) assert.are.equal(client1.id, data.credential.id) end) it("returns success with a dotted scope and store authenticated user properties", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", client_id = "clientid123", scope = "user.email", response_type = "code", state = "hello", authenticated_userid = "userid123" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = cjson.decode(assert.res_status(200, res)) assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}&state=hello$")) local iterator, err = ngx.re.gmatch(body.redirect_uri, "^http://google\\.com/kong\\?code=([\\w]{32,32})&state=hello$") assert.is_nil(err) local m, err = iterator() assert.is_nil(err) local data = db.oauth2_authorization_codes:select_by_code(m[1]) assert.are.equal(m[1], data.code) assert.are.equal("userid123", data.authenticated_userid) assert.are.equal("user.email", data.scope) end) it("fails when code challenge method is not supported", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", client_id = "clientid11211", scope = "user.email", response_type = "code", state = "hello", authenticated_userid = "userid123", code_challenge = "1234", code_challenge_method = "foo", }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ redirect_uri = "http://google.com/kong?error=invalid_request&error_description=code_challenge_method%20is%20not%20supported%2c%20must%20be%20S256&state=hello" }, json) end) it("fails when code challenge method is provided without code challenge", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", client_id = "clientid11211", scope = "user.email", response_type = "code", state = "hello", authenticated_userid = "userid123", code_challenge_method = "H256", }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json", } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ redirect_uri = "http://google.com/kong?error=invalid_request&error_description=code_challenge%20is%20required%20when%20code_method%20is%20present&state=hello" }, json) end) it("fails when code challenge is not included for public client", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", client_id = "clientid11211", scope = "user.email", response_type = "code", state = "hello", authenticated_userid = "userid123", }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ redirect_uri = "http://google.com/kong?error=invalid_request&error_description=code_challenge%20is%20required%20for%20public%20clients&state=hello" }, json) end) it("fails when code challenge is not included for confidential client when conf.pkce is strict", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", client_id = "clientid123", scope = "user.email", response_type = "code", state = "hello", authenticated_userid = "userid123", }, headers = { ["Host"] = "oauth2_15.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ redirect_uri = "http://google.com/kong?error=invalid_request&error_description=code_challenge%20is%20required%20for%20public%20clients&state=hello" }, json) end) it("returns success when code challenge is not included for public client when conf.pkce is none", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", client_id = "clientid11211", scope = "user.email", response_type = "code", state = "hello", authenticated_userid = "userid123", }, headers = { ["Host"] = "oauth2_14.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) local iterator, err = ngx.re.gmatch(json.redirect_uri, "^http://google\\.com/kong\\?code=([\\w]{32,32})&state=hello$") assert.is_nil(err) local m, err = iterator() assert.is_nil(err) db.oauth2_authorization_codes:select_by_code(m[1]) end) it("returns success and defaults code method to S256 when not provided", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", client_id = "clientid11211", scope = "user.email", response_type = "code", state = "hello", authenticated_userid = "userid123", code_challenge = "1234", }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) local iterator, err = ngx.re.gmatch(json.redirect_uri, "^http://google\\.com/kong\\?code=([\\w]{32,32})&state=hello$") assert.is_nil(err) local m, err = iterator() assert.is_nil(err) local data = db.oauth2_authorization_codes:select_by_code(m[1]) assert.are.equal("1234", data.challenge) assert.are.equal("S256", data.challenge_method) end) it("returns success and saves code challenge", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", client_id = "clientid11211", scope = "user.email", response_type = "code", state = "hello", authenticated_userid = "userid123", code_challenge = "1234", code_challenge_method = "S256", }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) local iterator, err = ngx.re.gmatch(json.redirect_uri, "^http://google\\.com/kong\\?code=([\\w]{32,32})&state=hello$") assert.is_nil(err) local m, err = iterator() assert.is_nil(err) local data = db.oauth2_authorization_codes:select_by_code(m[1]) assert.are.equal("1234", data.challenge) assert.are.equal("S256", data.challenge_method) end) end) describe("Implicit Grant", function() it("returns success", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "token" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = cjson.decode(assert.res_status(200, res)) assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\#access_token=[\\w]{32,32}&expires_in=[\\d]+&token_type=bearer$")) assert.are.equal("no-store", res.headers["cache-control"]) assert.are.equal("no-cache", res.headers["pragma"]) end) it("returns success and the state", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "token", state = "wot" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = cjson.decode(assert.res_status(200, res)) assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\#access_token=[\\w]{32,32}&expires_in=[\\d]+&state=wot&token_type=bearer$")) end) it("returns success and the token should have the right expiration", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "token" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = cjson.decode(assert.res_status(200, res)) assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\#access_token=[\\w]{32,32}&expires_in=[\\d]+&token_type=bearer$")) local iterator, err = ngx.re.gmatch(body.redirect_uri, "^http://google\\.com/kong\\#access_token=([\\w]{32,32})&expires_in=[\\d]+&token_type=bearer$") assert.is_nil(err) local m, err = iterator() assert.is_nil(err) local data = db.oauth2_tokens:select_by_access_token(m[1]) assert.are.equal(m[1], data.access_token) assert.are.equal(5, data.expires_in) assert.falsy(data.refresh_token) end) it("returns success and store authenticated user properties", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", client_id = "clientid123", scope = "email profile", response_type = "token", authenticated_userid = "userid123" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = cjson.decode(assert.res_status(200, res)) assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\#access_token=[\\w]{32,32}&expires_in=[\\d]+&token_type=bearer$")) local iterator, err = ngx.re.gmatch(body.redirect_uri, "^http://google\\.com/kong\\#access_token=([\\w]{32,32})&expires_in=[\\d]+&token_type=bearer$") assert.is_nil(err) local m, err = iterator() assert.is_nil(err) local data = db.oauth2_tokens:select_by_access_token(m[1]) assert.are.equal(m[1], data.access_token) assert.are.equal("userid123", data.authenticated_userid) assert.are.equal("email profile", data.scope) -- Checking that there is no refresh token since it's an implicit grant assert.are.equal(5, data.expires_in) assert.falsy(data.refresh_token) end) it("returns set the right upstream headers", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", client_id = "clientid123", scope = "email profile", response_type = "token", authenticated_userid = "userid123" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = cjson.decode(assert.res_status(200, res)) local iterator, err = ngx.re.gmatch(body.redirect_uri, "^http://google\\.com/kong\\#access_token=([\\w]{32,32})&expires_in=[\\d]+&token_type=bearer$") assert.is_nil(err) local m, err = iterator() assert.is_nil(err) local access_token = m[1] local res = assert(proxy_ssl_client:send { method = "GET", path = "/request?access_token=" .. access_token, headers = { ["Host"] = "oauth2.com" } }) local body = cjson.decode(assert.res_status(200, res)) assert.truthy(body.headers["x-consumer-id"]) assert.are.equal("bob", body.headers["x-consumer-username"]) assert.are.equal("email profile", body.headers["x-authenticated-scope"]) assert.are.equal("userid123", body.headers["x-authenticated-userid"]) assert.are.equal("clientid123", body.headers["x-credential-identifier"]) end) end) describe("Client Credentials", function() it("returns an error when client_secret is not sent", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { client_id = "clientid123", scope = "email", response_type = "token" }, headers = { ["Host"] = "oauth2_4.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json) end) it("returns an error when empty client_id and empty client_secret is sent", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { client_id = "", client_secret = "", scope = "email", response_type = "token", grant_type = "client_credentials", }, headers = { ["Host"] = "oauth2_4.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json) end) it("returns an error when missing client_id and missing client_secret is sent", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { scope = "email", response_type = "token", grant_type = "client_credentials", }, headers = { ["Host"] = "oauth2_4.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json) end) it("returns an error when empty client_id and empty client_secret is sent regardless of method", function() local res = assert(proxy_ssl_client:send { method = "GET", path = "/oauth2/token?client_id&grant_type=client_credentials&client_secret", body = {}, headers = { ["Host"] = "oauth2_4.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(405, res) local json = cjson.decode(body) assert.same({ error_description = "The HTTP method GET is invalid for the token endpoint", error = "invalid_method" }, json) end) it("returns an error when grant_type is not sent", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { client_id = "clientid123", client_secret = "secret123", scope = "email", response_type = "token" }, headers = { ["Host"] = "oauth2_4.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error = "unsupported_grant_type", error_description = "Invalid grant_type" }, json) end) it("fails when not under HTTPS", function() local res = assert(proxy_client:send { method = "POST", path = "/oauth2/token", body = { client_id = "clientid123", client_secret = "secret123", scope = "email", grant_type = "client_credentials" }, headers = { ["Host"] = "oauth2_4.com", ["Content-Type"] = "application/json" } }) assert.response(res).has.status(400) local json = assert.response(res).has.jsonbody(res) assert.equal("You must use HTTPS", json.error_description) assert.equal("access_denied", json.error) end) it("fails when setting authenticated_userid and no provision_key", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { client_id = "clientid123", client_secret = "secret123", scope = "email", grant_type = "client_credentials", authenticated_userid = "user123" }, headers = { ["Host"] = "oauth2_4.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid provision_key", error = "invalid_provision_key" }, json) end) it("fails when setting authenticated_userid and invalid provision_key", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { client_id = "clientid123", client_secret = "secret123", scope = "email", grant_type = "client_credentials", authenticated_userid = "user123", provision_key = "hello" }, headers = { ["Host"] = "oauth2_4.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid provision_key", error = "invalid_provision_key" }, json) end) it("returns success", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { client_id = "clientid123", client_secret = "secret123", scope = "email", grant_type = "client_credentials" }, headers = { ["Host"] = "oauth2_4.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) end) it("returns success with an application that has multiple redirect_uri", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { client_id = "clientid456", client_secret = "secret456", scope = "email", grant_type = "client_credentials" }, headers = { ["Host"] = "oauth2_4.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) end) it("returns success with an application that has multiple redirect_uri, and by passing a valid redirect_uri", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { client_id = "clientid456", client_secret = "secret456", scope = "email", grant_type = "client_credentials", redirect_uri = "http://two.com/two" }, headers = { ["Host"] = "oauth2_4.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) end) it("returns success with an application that has not redirect_uri", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { client_id = "clientid10112", client_secret = "secret10112", scope = "email", grant_type = "client_credentials", }, headers = { ["Host"] = "oauth2_4.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) end) it("returns success with authenticated_userid and valid provision_key", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { client_id = "clientid123", client_secret = "secret123", scope = "email", grant_type = "client_credentials", authenticated_userid = "hello", provision_key = "provision123" }, headers = { ["Host"] = "oauth2_4.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) end) it("returns success with authorization header", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { scope = "email", grant_type = "client_credentials" }, headers = { ["Host"] = "oauth2_4.com", ["Content-Type"] = "application/json", Authorization = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTIz" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) end) it("returns success with authorization header and client_id body param", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { client_id = "clientid123", scope = "email", grant_type = "client_credentials" }, headers = { ["Host"] = "oauth2_4.com", ["Content-Type"] = "application/json", Authorization = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTIz" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) end) it("returns an error with a wrong authorization header", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { scope = "email", grant_type = "client_credentials" }, headers = { ["Host"] = "oauth2_4.com", ["Content-Type"] = "application/json", Authorization = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTI0" } }) local body = assert.res_status(401, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json) assert.are.equal("Basic realm=\"OAuth2.0\"", res.headers["www-authenticate"]) end) it("sets the right upstream headers", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { client_id = "clientid123", client_secret = "secret123", scope = "email", grant_type = "client_credentials", authenticated_userid = "hello", provision_key = "provision123" }, headers = { ["Host"] = "oauth2_4.com", ["Content-Type"] = "application/json" } }) local body = cjson.decode(assert.res_status(200, res)) local res = assert(proxy_ssl_client:send { method = "GET", path = "/request?access_token=" .. body.access_token, headers = { ["Host"] = "oauth2_4.com" } }) local body = cjson.decode(assert.res_status(200, res)) assert.truthy(body.headers["x-consumer-id"]) assert.are.equal("bob", body.headers["x-consumer-username"]) assert.are.equal("email", body.headers["x-authenticated-scope"]) assert.are.equal("hello", body.headers["x-authenticated-userid"]) assert.are.equal("clientid123", body.headers["x-credential-identifier"]) end) it("works in a multipart request", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { client_id = "clientid123", client_secret = "secret123", scope = "email", grant_type = "client_credentials", authenticated_userid = "hello", provision_key = "provision123" }, headers = { ["Host"] = "oauth2_4.com", ["Content-Type"] = "multipart/form-data" } }) local body = cjson.decode(assert.res_status(200, res)) local res = assert(proxy_ssl_client:send { method = "POST", path = "/request", body = { access_token = body.access_token }, headers = { ["Host"] = "oauth2_4.com", ["Content-Type"] = "multipart/form-data" } }) assert.res_status(200, res) end) end) describe("Password Grant", function() it("blocks unauthorized requests", function() local res = assert(proxy_ssl_client:send { method = "GET", path = "/request", headers = { ["Host"] = "oauth2_5.com" } }) local body = assert.res_status(401, res) local json = cjson.decode(body) assert.same({ error_description = "The access token is missing", error = "invalid_request" }, json) end) it("returns an error when client_secret is not sent", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { client_id = "clientid123", scope = "email", response_type = "token" }, headers = { ["Host"] = "oauth2_5.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json) end) it("returns an error when grant_type is not sent", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { client_id = "clientid123", client_secret = "secret123", scope = "email", response_type = "token" }, headers = { ["Host"] = "oauth2_5.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error = "unsupported_grant_type", error_description = "Invalid grant_type" }, json) end) it("fails when no provision key is being sent", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { client_id = "clientid123", client_secret = "secret123", scope = "email", response_type = "token", grant_type = "password" }, headers = { ["Host"] = "oauth2_5.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid provision_key", error = "invalid_provision_key" }, json) end) it("fails when no provision key is being sent", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { client_id = "clientid123", client_secret = "secret123", scope = "email", grant_type = "password" }, headers = { ["Host"] = "oauth2_5.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid provision_key", error = "invalid_provision_key" }, json) end) it("fails when no authenticated user id is being sent", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { provision_key = "provision123", client_id = "clientid123", client_secret = "secret123", scope = "email", grant_type = "password" }, headers = { ["Host"] = "oauth2_5.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Missing authenticated_userid parameter", error = "invalid_authenticated_userid" }, json) end) it("returns success", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", client_secret = "secret123", scope = "email", grant_type = "password" }, headers = { ["Host"] = "oauth2_5.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) assert.equal(32, #json.refresh_token) assert.matches("%w+", json.refresh_token) end) it("returns success with authorization header", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { provision_key = "provision123", authenticated_userid = "id123", scope = "email", grant_type = "password" }, headers = { ["Host"] = "oauth2_5.com", ["Content-Type"] = "application/json", Authorization = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTIz" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) assert.equal(32, #json.refresh_token) assert.matches("%w+", json.refresh_token) end) it("returns an error with a wrong authorization header", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { provision_key = "provision123", authenticated_userid = "id123", scope = "email", grant_type = "password" }, headers = { ["Host"] = "oauth2_5.com", ["Content-Type"] = "application/json", Authorization = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTI0" } }) local body = assert.res_status(401, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json) assert.are.equal("Basic realm=\"OAuth2.0\"", res.headers["www-authenticate"]) end) it("sets the right upstream headers", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { provision_key = "provision123", authenticated_userid = "id123", scope = "email", grant_type = "password" }, headers = { ["Host"] = "oauth2_5.com", ["Content-Type"] = "application/json", Authorization = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTIz" } }) local body = cjson.decode(assert.res_status(200, res)) local res = assert(proxy_ssl_client:send { method = "GET", path = "/request?access_token=" .. body.access_token, headers = { ["Host"] = "oauth2_5.com" } }) local body = cjson.decode(assert.res_status(200, res)) assert.truthy(body.headers["x-consumer-id"]) assert.are.equal("bob", body.headers["x-consumer-username"]) assert.are.equal("email", body.headers["x-authenticated-scope"]) assert.are.equal("id123", body.headers["x-authenticated-userid"]) assert.are.equal("clientid123", body.headers["x-credential-identifier"]) end) end) end) describe("OAuth2 Access Token", function() it("returns an error when nothing is being sent", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", headers = { ["Host"] = "oauth2.com" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json) -- Checking headers assert.are.equal("no-store", res.headers["cache-control"]) assert.are.equal("no-cache", res.headers["pragma"]) end) it("returns an error when only the code is being sent", function() local code = provision_code() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json) -- Checking headers assert.are.equal("no-store", res.headers["cache-control"]) assert.are.equal("no-cache", res.headers["pragma"]) end) it("returns an error when only the code and client_secret are being sent", function() local code = provision_code() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_secret = "secret123" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json) -- Checking headers assert.are.equal("no-store", res.headers["cache-control"]) assert.are.equal("no-cache", res.headers["pragma"]) end) it("returns an error when grant_type is not being sent", function() local code = provision_code() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid123", client_secret = "secret123" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error = "unsupported_grant_type", error_description = "Invalid grant_type" }, json) end) it("returns an error with a wrong code", function() local code = provision_code() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code .. "hello", client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error = "invalid_request", error_description = "Invalid code" }, json) end) it("returns success without state", function() local code = provision_code() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) assert.equal(32, #json.refresh_token) assert.matches("%w+", json.refresh_token) end) it("returns success with state", function() local code = provision_code() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code", state = "wot" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) assert.equal(32, #json.refresh_token) assert.matches("%w+", json.refresh_token) end) it("fails when the client used for the code is not the same client used for the token", function() local code = provision_code(nil, nil, "clientid333") local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code", state = "wot" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) assert.same({ error = "invalid_request", error_description = "Invalid code", state = "wot" }, cjson.decode(body)) end) it("sets the right upstream headers", function() local code = provision_code() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = cjson.decode(assert.res_status(200, res)) local res = assert(proxy_ssl_client:send { method = "GET", path = "/request?access_token=" .. body.access_token, headers = { ["Host"] = "oauth2.com" } }) local body = cjson.decode(assert.res_status(200, res)) assert.truthy(body.headers["x-consumer-id"]) assert.are.equal("bob", body.headers["x-consumer-username"]) assert.are.equal("email", body.headers["x-authenticated-scope"]) assert.are.equal("userid123", body.headers["x-authenticated-userid"]) assert.are.equal("clientid123", body.headers["x-credential-identifier"]) end) it("fails when an authorization code is used more than once", function() local code = provision_code() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) assert.equal(32, #json.refresh_token) assert.matches("%w+", json.refresh_token) local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error = "invalid_request", error_description = "Invalid code" }, json) end) it("fails when an authorization code is used by another application", function() local code = provision_code() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid789", client_secret = "secret789", grant_type = "authorization_code" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error = "invalid_request", error_description = "Invalid code" }, json) end) it("fails when an authorization code is used for another API", function() local code = provision_code() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, headers = { ["Host"] = "oauth2_3.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error = "invalid_request", error_description = "Invalid code" }, json) end) it("succeeds when using code challenge", function() local challenge, verifier = get_pkce_tokens() local code = provision_code(nil, nil, "clientid11211", challenge) local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid11211", grant_type = "authorization_code", code_verifier = verifier }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) assert.equal(32, #json.refresh_token) assert.matches("%w+", json.refresh_token) end) it("succeeds when authorization header used for public app", function() local challenge, verifier = get_pkce_tokens() local code = provision_code(nil, nil, "clientid11211", challenge) local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, grant_type = "authorization_code", code_verifier = verifier }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json", Authorization = "Basic Y2xpZW50aWQxMTIxMQ==" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) assert.equal(32, #json.refresh_token) assert.matches("%w+", json.refresh_token) end) it("succeeds when authorization header used for public app with colon", function() local challenge, verifier = get_pkce_tokens() local code = provision_code(nil, nil, "clientid11211", challenge) local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, grant_type = "authorization_code", code_verifier = verifier }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json", Authorization = "Basic Y2xpZW50aWQxMTIxMTo=" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) assert.equal(32, #json.refresh_token) assert.matches("%w+", json.refresh_token) end) it("succeeds when authorization header used for public app with empty secret", function() local challenge, verifier = get_pkce_tokens() local code = provision_code(nil, nil, "clientid11211", challenge) local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, grant_type = "authorization_code", code_verifier = verifier }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json", Authorization = "Basic Y2xpZW50aWQxMTIxMTogICAg" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) assert.equal(32, #json.refresh_token) assert.matches("%w+", json.refresh_token) end) it("fails when a secret provided for public app", function() local challenge, verifier = get_pkce_tokens() local code = provision_code(nil, nil, "clientid11211", challenge) local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid11211", grant_type = "authorization_code", code_verifier = verifier, client_secret = "secret11211" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "client_secret is disallowed for public clients", error = "invalid_request" }, json) end) it("fails when a secret provided for public app in header", function() local challenge, verifier = get_pkce_tokens() local code = provision_code(nil, nil, "clientid11211", challenge) local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, grant_type = "authorization_code", code_verifier = verifier, }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json", Authorization = "Basic Y2xpZW50aWQxMTIxMTpzZWNyZXQxMTIxMQ==" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "client_secret is disallowed for public clients", error = "invalid_request" }, json) end) it("fails when no code_verifier provided for public app", function() local challenge, _ = get_pkce_tokens() local code = provision_code(nil, nil, "clientid11211", challenge) local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid11211", grant_type = "authorization_code", }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "code_verifier is required for PKCE authorization requests", error = "invalid_request" }, json) end) it("success when no code_verifier provided for public app without pkce when conf.pkce is none", function() local code = provision_code() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code", }, headers = { ["Host"] = "oauth2_14.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) assert.equal(32, #json.refresh_token) assert.matches("%w+", json.refresh_token) end) it("success when code challenge contains padding", function() local code_verifier = "abcdelfhigklmnopqrstuvwxyz0123456789abcdefg" local code_challenge = "2aC4cMSkAsMRtZbhHhiZkDW3sddRf_iTRGil1r9gi8w=" local code = provision_code(nil, nil, "clientid11211", code_challenge) local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid11211", grant_type = "authorization_code", code_verifier = code_verifier }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) assert.equal(32, #json.refresh_token) assert.matches("%w+", json.refresh_token) end) it("succeeds when code challenge contains + or / characters", function() local code_verifier = "abcdelfhigklmnopqrstuvwxyz0123456789abcdefghijklmnop9" local code_challenge = "0LoS6Gtrw16r07+ZXsCf8MeAi21QHmKc3LJdUCA5w/o=" local code = provision_code(nil, nil, "clientid11211", code_challenge) local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid11211", grant_type = "authorization_code", code_verifier = code_verifier }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) assert.equal(32, #json.refresh_token) assert.matches("%w+", json.refresh_token) end) it("fails when code verifier is greater than 128 characters", function() local code_verifier = string_rep("abc123", 30) local challenge, verifier = get_pkce_tokens(code_verifier) local code = provision_code(nil, nil, "clientid11211", challenge) local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid11211", grant_type = "authorization_code", code_verifier = verifier }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "code_verifier must be between 43 and 128 characters", error = "invalid_request" }, json) end) it("fails when code verifier is less than 43 characters", function() local challenge, verifier = get_pkce_tokens("abc123") local code = provision_code(nil, nil, "clientid11211", challenge) local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid11211", grant_type = "authorization_code", code_verifier = verifier }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "code_verifier must be between 43 and 128 characters", error = "invalid_request" }, json) end) it("fails when code verifier is missing", function() local challenge, _ = get_pkce_tokens("abc123") local code = provision_code(nil, nil, "clientid11211", challenge) local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid11211", grant_type = "authorization_code", }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "code_verifier is required for PKCE authorization requests", error = "invalid_request" }, json) end) it("fails when secret does not match for non-authorization_code grant type", function() local challenge, _ = get_pkce_tokens() local code = provision_code(nil, nil, "clientid11211", challenge) local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", grant_type = "password", client_secret = "bogus", code = code }, headers = { ["Host"] = "oauth2_5.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json) end) it("fails when code verifier is empty", function() local code = provision_code(nil, nil, "clientid11211", "abc123") local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid11211", grant_type = "authorization_code", code_verifier = "" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "code_verifier must be between 43 and 128 characters", error = "invalid_request" }, json) end) it("fails when code verifier is not a string", function() local code = provision_code(nil, nil, "clientid11211", "abc123") local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid11211", grant_type = "authorization_code", code_verifier = 12 }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "code_verifier is not a string", error = "invalid_request" }, json) end) it("fails when code verifier does not match challenge", function() local code = provision_code(nil, nil, "clientid11211", "abc123") local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid11211", grant_type = "authorization_code", code_verifier = "abcdelfhigklmnopqrstuvwxyz0123456789abcdefg" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid code", error = "invalid_grant" }, json) end) it("fails when code verifier does not match challenge for confidential app when conf.pkce is strict", function() local challenge, _ = get_pkce_tokens() local code = provision_code(nil, nil, nil, challenge) local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code", code_verifier = "abcdelfhigklmnopqrstuvwxyz0123456789abcdefg" }, headers = { ["Host"] = "oauth2_15.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid code", error = "invalid_grant" }, json) end) it("fails when wrong auth code provided for public app", function() local challenge, verifier = get_pkce_tokens() local code = provision_code(nil, nil, "clientid11211", challenge) local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code .. "hello", client_id = "clientid11211", grant_type = "authorization_code", code_verifier = verifier, }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid code", error = "invalid_request" }, json) end) it("fails when no auth code provided for public app", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { client_id = "clientid11211", grant_type = "authorization_code", code_verifier = "verifier", }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid code", error = "invalid_request" }, json) end) it("fails when no secret provided for confidential app", function() local code = provision_code() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid123", grant_type = "authorization_code", }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json) end) it("fails when no code verifier provided for confidential app when conf.pkce is strict", function() local code = provision_code() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code", }, headers = { ["Host"] = "oauth2_15.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "code_verifier is required for PKCE authorization requests", error = "invalid_request" }, json) end) it("fails when no code verifier provided for confidential app with pkce when conf.pkce is lax", function() local challenge, _ = get_pkce_tokens() local code = provision_code(nil, nil, nil, challenge) local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code", }, headers = { ["Host"] = "oauth2_16.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "code_verifier is required for PKCE authorization requests", error = "invalid_request" }, json) end) it("fails when no code verifier provided for confidential app with pkce when conf.pkce is none", function() local challenge, _ = get_pkce_tokens() local code = provision_code(nil, nil, nil, challenge) local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code", }, headers = { ["Host"] = "oauth2_14.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "code_verifier is required for PKCE authorization requests", error = "invalid_request" }, json) end) it("suceeds when no code verifier provided for confidential app without pkce when conf.pkce is none", function() local code = provision_code() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code", }, headers = { ["Host"] = "oauth2_14.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) assert.equal(32, #json.refresh_token) assert.matches("%w+", json.refresh_token) end) it("suceeds when no code verifier provided for confidential app without pkce when conf.pkce is lax", function() local code = provision_code() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code", }, headers = { ["Host"] = "oauth2_16.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) assert.equal(32, #json.refresh_token) assert.matches("%w+", json.refresh_token) end) end) describe("Making a request", function() it("fails when no access_token is being sent in an application/json body", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/request", headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(401, res) local json = cjson.decode(body) assert.same({ error_description = "The access token is missing", error = "invalid_request" }, json) end) it("works when a correct access_token is being sent in the querystring", function() local token = provision_token() local res = assert(proxy_ssl_client:send { method = "GET", path = "/request?access_token=" .. token.access_token, headers = { ["Host"] = "oauth2.com" } }) assert.res_status(200, res) end) it("works when a correct access_token is being sent in the custom header", function() local token = provision_token("oauth2_11.com",nil,"clientid1011","secret1011") local res = assert(proxy_ssl_client:send { method = "GET", path = "/request", headers = { ["Host"] = "oauth2_11.com", ["custom_header_name"] = "bearer " .. token.access_token, } }) assert.res_status(200, res) end) it("works when a correct access_token is being sent in duplicate custom headers", function() local token = provision_token("oauth2_11.com",nil,"clientid1011","secret1011") local res = assert(proxy_ssl_client:send { method = "GET", path = "/request", headers = { ["Host"] = "oauth2_11.com", ["custom_header_name"] = { "bearer " .. token.access_token, "bearer " .. token.access_token }, } }) assert.res_status(200, res) end) it("fails when missing access_token is being sent in the custom header", function() local res = assert(proxy_ssl_client:send { method = "GET", path = "/request", headers = { ["Host"] = "oauth2_11.com", ["custom_header_name"] = "", } }) assert.res_status(401, res) end) it("fails when a correct access_token is being sent in the wrong header", function() local token = provision_token("oauth2_11.com",nil,"clientid1011","secret1011") local res = assert(proxy_ssl_client:send { method = "GET", path = "/request", headers = { ["Host"] = "oauth2_11.com", ["authorization"] = "bearer " .. token.access_token, } }) assert.res_status(401, res) end) it("does not work when requesting a different API", function() local token = provision_token() local res = assert(proxy_ssl_client:send { method = "GET", path = "/request?access_token=" .. token.access_token, headers = { ["Host"] = "oauth2_3.com" } }) local body = assert.res_status(401, res) local json = cjson.decode(body) assert.same({ error_description = "The access token is invalid or has expired", error = "invalid_token" }, json) end) it("works when a correct access_token is being sent in a form body", function() local token = provision_token() local res = assert(proxy_ssl_client:send { method = "POST", path = "/request", body = { access_token = token.access_token }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) assert.res_status(200, res) end) it("does not throw an error when request has no body", function() -- Regression test for the following issue: -- https://github.com/Kong/kong/issues/3055 -- -- We want to make sure we do not attempt to parse a -- request body if the request isn't supposed to have -- one in the first place. helpers.clean_logfile() -- TEST: access with a GET request local token = provision_token() local res = assert(proxy_ssl_client:send { method = "GET", path = "/request?access_token=" .. token.access_token, headers = { ["Host"] = "oauth2.com" } }) assert.res_status(200, res) -- Assertion: there should be no [error], including no error -- resulting from an invalid request body parsing that were -- previously thrown. assert.logfile().has.no.line("[error]", true) end) it("works when a correct access_token is being sent in an authorization header (bearer)", function() local token = provision_token() local res = assert(proxy_ssl_client:send { method = "POST", path = "/request", headers = { ["Host"] = "oauth2.com", Authorization = "bearer " .. token.access_token } }) assert.res_status(200, res) end) it("works when a correct access_token is being sent in an authorization header (token)", function() local token = provision_token() local res = assert(proxy_ssl_client:send { method = "POST", path = "/request", headers = { ["Host"] = "oauth2.com", Authorization = "bearer " .. token.access_token } }) local body = cjson.decode(assert.res_status(200, res)) local consumer = db.consumers:select_by_username("bob") assert.are.equal(consumer.id, body.headers["x-consumer-id"]) assert.are.equal(consumer.username, body.headers["x-consumer-username"]) assert.are.equal("userid123", body.headers["x-authenticated-userid"]) assert.are.equal("email", body.headers["x-authenticated-scope"]) assert.are.equal("clientid123", body.headers["x-credential-identifier"]) assert.is_nil(body.headers["x-anonymous-consumer"]) end) it("accepts gRPC call with credentials", function() local token = provision_token("oauth2_grpc.com") local ok, res = helpers.proxy_client_grpcs(){ service = "hello.HelloService.SayHello", opts = { ["-authority"] = "oauth2_grpc.com", ["-H"] = ("'authorization: bearer %s'"):format(token.access_token), }, } assert.truthy(ok) assert.same({ reply = "hello noname" }, cjson.decode(res)) end) it("returns HTTP 400 when scope is not a string", function() local invalid_values = { { "email" }, 123, false, } for _, val in ipairs(invalid_values) do local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", client_secret="secret123", scope = val, grant_type = "password", }, headers = { ["Host"] = "oauth2_5.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error = "invalid_scope", error_description = "scope must be a string" }, json) end end) it("works with right credentials and anonymous", function() local token = provision_token("oauth2_7.com") local res = assert(proxy_ssl_client:send { method = "POST", path = "/request", headers = { ["Host"] = "oauth2_7.com", Authorization = "bearer " .. token.access_token } }) local body = cjson.decode(assert.res_status(200, res)) local consumer = db.consumers:select_by_username("bob") assert.are.equal(consumer.id, body.headers["x-consumer-id"]) assert.are.equal(consumer.username, body.headers["x-consumer-username"]) assert.are.equal("userid123", body.headers["x-authenticated-userid"]) assert.are.equal("email", body.headers["x-authenticated-scope"]) assert.are.equal("clientid123", body.headers["x-credential-identifier"]) assert.is_nil(body.headers["x-anonymous-consumer"]) end) it("works with wrong credentials and anonymous", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/request", headers = { ["Host"] = "oauth2_7.com" } }) local body = cjson.decode(assert.res_status(200, res)) assert.are.equal("true", body.headers["x-anonymous-consumer"]) assert.equal('no-body', body.headers["x-consumer-username"]) assert.are.equal(nil, body.headers["x-credential-identifier"]) end) it("works with wrong credentials and username in anonymous", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/request", headers = { ["Host"] = "oauth2__c.com" } }) local body = cjson.decode(assert.res_status(200, res)) assert.are.equal("true", body.headers["x-anonymous-consumer"]) assert.equal('no-body', body.headers["x-consumer-username"]) end) it("errors when anonymous user doesn't exist", function() finally(function() if proxy_ssl_client then proxy_ssl_client:close() end proxy_ssl_client = helpers.proxy_ssl_client() end) local res = assert(proxy_ssl_client:send { method = "GET", path = "/request", headers = { ["Host"] = "oauth2_10.com" } }) assert.res_status(500, res) end) it("returns success and the token should have the right expiration when a custom header is passed", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid1011", scope = "email", response_type = "token" }, headers = { ["Host"] = "oauth2_11.com", ["Content-Type"] = "application/json" } }) local body = cjson.decode(assert.res_status(200, res)) assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\#access_token=[\\w]{32,32}&expires_in=[\\d]+&token_type=bearer$")) local iterator, err = ngx.re.gmatch(body.redirect_uri, "^http://google\\.com/kong\\#access_token=([\\w]{32,32})&expires_in=[\\d]+&token_type=bearer$") assert.is_nil(err) local m, err = iterator() assert.is_nil(err) local data = db.oauth2_tokens:select_by_access_token(m[1]) assert.are.equal(m[1], data.access_token) assert.are.equal(7, data.expires_in) assert.falsy(data.refresh_token) end) describe("Global Credentials", function() it("does not access two different APIs that are not sharing global credentials", function() local token = provision_token("oauth2_8.com") local res = assert(proxy_ssl_client:send { method = "POST", path = "/request", headers = { ["Host"] = "oauth2_8.com", Authorization = "bearer " .. token.access_token } }) assert.res_status(200, res) local res = assert(proxy_ssl_client:send { method = "POST", path = "/request", headers = { ["Host"] = "oauth2.com", Authorization = "bearer " .. token.access_token } }) assert.res_status(401, res) end) it("does not access two different APIs that are not sharing global credentials 2", function() local token = provision_token("oauth2.com") local res = assert(proxy_ssl_client:send { method = "POST", path = "/request", headers = { ["Host"] = "oauth2_8.com", Authorization = "bearer " .. token.access_token } }) assert.res_status(401, res) local res = assert(proxy_ssl_client:send { method = "POST", path = "/request", headers = { ["Host"] = "oauth2.com", Authorization = "bearer " .. token.access_token } }) assert.res_status(200, res) end) it("access two different APIs that are sharing global credentials", function() local token = provision_token("oauth2_8.com") local res = assert(proxy_ssl_client:send { method = "POST", path = "/request", headers = { ["Host"] = "oauth2_8.com", Authorization = "bearer " .. token.access_token } }) assert.res_status(200, res) local res = assert(proxy_ssl_client:send { method = "POST", path = "/request", headers = { ["Host"] = "oauth2_9.com", Authorization = "bearer " .. token.access_token } }) assert.res_status(200, res) end) end) end) describe("Authentication challenge", function() it("returns 401 Unauthorized without error if it lacks any authentication information", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/request", headers = { ["Host"] = "oauth2.com" } }) local body = assert.res_status(401, res) local json = cjson.decode(body) assert.same({ error_description = "The access token is missing", error = "invalid_request" }, json) assert.are.equal('Bearer realm="service"', res.headers['www-authenticate']) end) it("returns 401 Unauthorized when an invalid access token is being sent via url parameter", function() local res = assert(proxy_ssl_client:send { method = "GET", path = "/request?access_token=invalid", headers = { ["Host"] = "oauth2.com" } }) local body = assert.res_status(401, res) local json = cjson.decode(body) assert.same({ error_description = "The access token is invalid or has expired", error = "invalid_token" }, json) assert.are.equal('Bearer realm="service" error="invalid_token" error_description="The access token is invalid or has expired"', res.headers['www-authenticate']) end) it("returns 401 Unauthorized when an invalid access token is being sent via the Authorization header", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/request", headers = { ["Host"] = "oauth2.com", Authorization = "bearer invalid" } }) local body = assert.res_status(401, res) local json = cjson.decode(body) assert.same({ error_description = "The access token is invalid or has expired", error = "invalid_token" }, json) assert.are.equal('Bearer realm="service" error="invalid_token" error_description="The access token is invalid or has expired"', res.headers['www-authenticate']) end) it("returns 401 Unauthorized when token has expired", function() local token = provision_token() -- Token expires in 5 seconds local status, json, headers helpers.wait_until(function() local client = helpers.proxy_ssl_client() local res = assert(client:send { method = "POST", path = "/request", headers = { ["Host"] = "oauth2.com", Authorization = "bearer " .. token.access_token } }) local body = res:read_body() status = res.status headers = res.headers json = cjson.decode(body) client:close() return status == 401 end, 7) assert.same({ error_description = "The access token is invalid or has expired", error = "invalid_token" }, json) assert.are.equal('Bearer realm="service" error="invalid_token" error_description="The access token is invalid or has expired"', headers['www-authenticate']) end) end) describe("Refresh Token", function() it("does not refresh an invalid access token", function() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { refresh_token = "hello", client_id = "clientid123", client_secret = "secret123", grant_type = "refresh_token" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error = "invalid_request", error_description = "Invalid refresh_token" }, json) end) it("refreshes an valid access token", function() local token = provision_token() local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { refresh_token = token.refresh_token, client_id = "clientid123", client_secret = "secret123", grant_type = "refresh_token" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) assert.equal(32, #json.refresh_token) assert.matches("%w+", json.refresh_token) end) it("refreshes public app without a secret", function() local challenge, verifier = get_pkce_tokens() local token = provision_token(nil, nil, "clientid11211", nil, challenge, verifier, false) local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { refresh_token = token.refresh_token, client_id = "clientid11211", grant_type = "refresh_token" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) assert.equal(32, #json.refresh_token) assert.matches("%w+", json.refresh_token) end) it("fails to refresh when a secret provided for public app", function() local challenge, verifier = get_pkce_tokens() local token = provision_token(nil, nil, "clientid11211", nil, challenge, verifier, false) local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { refresh_token = token.refresh_token, client_id = "clientid11211", client_secret = "secret11211", grant_type = "refresh_token" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "client_secret is disallowed for public clients", error = "invalid_request" }, json) end) it("fails to refresh when no secret provided for confidential app", function() local token = provision_token(nil, nil, "clientid123") local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { refresh_token = token.refresh_token, client_id = "clientid123", grant_type = "refresh_token" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json) end) it("refreshes an valid access token and checks that it belongs to the application", function() local token = provision_token(nil, nil, "clientid333", "secret333") local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { refresh_token = token.refresh_token, client_id = "clientid123", client_secret = "secret123", grant_type = "refresh_token" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(400, res) local json = cjson.decode(body) assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json) assert.are.equal("no-store", res.headers["cache-control"]) assert.are.equal("no-cache", res.headers["pragma"]) end) it("expires after 5 seconds", function() local token = provision_token() local res = assert(proxy_client:send { method = "POST", path = "/request", headers = { ["Host"] = "oauth2.com", authorization = "bearer " .. token.access_token } }) assert.res_status(200, res) local id = db.oauth2_tokens:select_by_access_token(token.access_token).id assert.truthy(db.oauth2_tokens:select({ id = id })) -- But waiting after the cache expiration (5 seconds) should block the request local status, json helpers.wait_until(function() local client = helpers.proxy_client() local res = assert(client:send { method = "POST", path = "/request", headers = { ["Host"] = "oauth2.com", authorization = "bearer " .. token.access_token } }) status = res.status local body = res:read_body() json = body and cjson.decode(body) return status == 401 end, 7) assert.same({ error_description = "The access token is invalid or has expired", error = "invalid_token" }, json) -- Refreshing the token local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { refresh_token = token.refresh_token, client_id = "clientid123", client_secret = "secret123", grant_type = "refresh_token" }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/json", authorization = "bearer " .. token.access_token } }) local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal("bearer", json.token_type) assert.equal(5, json.expires_in) assert.equal(32, #json.access_token) assert.matches("%w+", json.access_token) assert.equal(32, #json.refresh_token) assert.matches("%w+", json.refresh_token) assert.falsy(token.access_token == cjson.decode(body).access_token) assert.falsy(token.refresh_token == cjson.decode(body).refresh_token) assert.falsy(db.oauth2_tokens:select({ id = id })) end) it("does rewrite non-persistent refresh tokens", function () local token = provision_token() local refreshed_token = refresh_token(nil, token.refresh_token) assert.is_table(refreshed_token) assert.falsy(token.refresh_token == refreshed_token.refresh_token) end) it("does not rewrite persistent refresh tokens", function() local token = provision_token("oauth2_13.com") local refreshed_token = refresh_token("oauth2_13.com", token.refresh_token) local new_access_token = db.oauth2_tokens:select_by_access_token(refreshed_token.access_token) local new_refresh_token = db.oauth2_tokens:select_by_refresh_token(token.refresh_token) assert.truthy(new_refresh_token) assert.same(new_access_token.id, new_refresh_token.id) -- check refreshing sets created_at so access token doesn't expire db.oauth2_tokens:update({ id = new_refresh_token.id }, { created_at = 123, -- set time as expired }) local status, json, headers helpers.wait_until(function() local client = helpers.proxy_ssl_client() local first_res = assert(client:send { method = "POST", path = "/request", headers = { ["Host"] = "oauth2_13.com", Authorization = "bearer " .. refreshed_token.access_token } }) local nbody = first_res:read_body() status = first_res.status headers = first_res.headers json = cjson.decode(nbody) client:close() return status == 401 end, 7) assert.same({ error_description = "The access token is invalid or has expired", error = "invalid_token" }, json) assert.are.equal('Bearer realm="service" error="invalid_token" error_description="The access token is invalid or has expired"', headers['www-authenticate']) local final_refreshed_token = refresh_token("oauth2_13.com", refreshed_token.refresh_token) local last_res = assert(proxy_client:send { method = "GET", path = "/request", headers = { ["Host"] = "oauth2_13.com", authorization = "bearer " .. final_refreshed_token.access_token } }) local last_body = cjson.decode(assert.res_status(200, last_res)) assert.equal("bearer " .. final_refreshed_token.access_token, last_body.headers.authorization) end) end) describe("Hide Credentials", function() it("does not hide credentials in the body", function() local token = provision_token() local res = assert(proxy_client:send { method = "POST", path = "/request", body = { access_token = token.access_token }, headers = { ["Host"] = "oauth2.com", ["Content-Type"] = "application/x-www-form-urlencoded" } }) local body = cjson.decode(assert.res_status(200, res)) assert.equal(token.access_token, body.post_data.params.access_token) end) it("hides credentials in the body", function() local token = provision_token("oauth2_3.com") local res = assert(proxy_client:send { method = "POST", path = "/request", body = { access_token = token.access_token }, headers = { ["Host"] = "oauth2_3.com", ["Content-Type"] = "application/x-www-form-urlencoded" } }) local body = cjson.decode(assert.res_status(200, res)) assert.is_nil(body.post_data.params.access_token) end) it("does not hide credentials in the querystring", function() local token = provision_token() local res = assert(proxy_client:send { method = "GET", path = "/request?access_token=" .. token.access_token, headers = { ["Host"] = "oauth2.com" } }) local body = cjson.decode(assert.res_status(200, res)) assert.equal(token.access_token, body.uri_args.access_token) end) it("hides credentials in the querystring", function() local token = provision_token("oauth2_3.com") local res = assert(proxy_client:send { method = "GET", path = "/request?access_token=" .. token.access_token, headers = { ["Host"] = "oauth2_3.com" } }) local body = cjson.decode(assert.res_status(200, res)) assert.is_nil(body.uri_args.access_token) end) it("hides credentials in the querystring for api with custom header", function() local token = provision_token("oauth2_12.com",nil,"clientid1011","secret1011") local res = assert(proxy_client:send { method = "GET", path = "/request?access_token=" .. token.access_token, headers = { ["Host"] = "oauth2_12.com" } }) local body = cjson.decode(assert.res_status(200, res)) assert.is_nil(body.uri_args.access_token) end) it("does not hide credentials in the header", function() local token = provision_token() local res = assert(proxy_client:send { method = "GET", path = "/request", headers = { ["Host"] = "oauth2.com", authorization = "bearer " .. token.access_token } }) local body = cjson.decode(assert.res_status(200, res)) assert.equal("bearer " .. token.access_token, body.headers.authorization) end) it("hides credentials in the header", function() local token = provision_token("oauth2_3.com") local res = assert(proxy_client:send { method = "GET", path = "/request", headers = { ["Host"] = "oauth2_3.com", authorization = "bearer " .. token.access_token } }) local body = cjson.decode(assert.res_status(200, res)) assert.is_nil(body.headers.authorization) end) it("hides credentials in the custom header", function() local token = provision_token("oauth2_12.com",nil,"clientid1011","secret1011") local res = assert(proxy_client:send { method = "GET", path = "/request", headers = { ["Host"] = "oauth2_12.com", ["custom_header_name"] = "bearer " .. token.access_token } }) local body = cjson.decode(assert.res_status(200, res)) assert.is_nil(body.headers.authorization) assert.is_nil(body.headers.custom_header_name) end) it("does not abort when the request body is a multipart form upload", function() local token = provision_token("oauth2_3.com") local res = assert(proxy_client:send { method = "POST", path = "/request?access_token=" .. token.access_token, body = { foo = "bar" }, headers = { ["Host"] = "oauth2_3.com", ["Content-Type"] = "multipart/form-data" } }) assert.res_status(200, res) end) end) end) describe("Plugin: oauth2 (access) [#" .. strategy .. "]", function() local proxy_client local user1 local user2 local anonymous local keyauth local jwt_secret lazy_setup(function() local service1 = admin_api.services:insert({ path = "/request" }) local route1 = assert(admin_api.routes:insert({ hosts = { "logical-and.com" }, protocols = { "http", "https" }, service = service1 })) admin_api.oauth2_plugins:insert({ route = { id = route1.id }, config = { scopes = { "email", "profile", "user.email" } }, }) admin_api.plugins:insert { name = "key-auth", route = { id = route1.id }, } anonymous = admin_api.consumers:insert { username = "Anonymous", } user1 = admin_api.consumers:insert { username = "Mickey", } user2 = admin_api.consumers:insert { username = "Aladdin", } local service2 = admin_api.services:insert({ path = "/request" }) local route2 = assert(admin_api.routes:insert({ hosts = { "logical-or.com" }, protocols = { "http", "https" }, service = service2 })) local route3 = assert(admin_api.routes:insert({ hosts = { "logical-or-jwt.com" }, protocols = { "http", "https" }, service = service2 })) admin_api.oauth2_plugins:insert({ route = { id = route2.id }, config = { scopes = { "email", "profile", "user.email" }, anonymous = anonymous.id, }, }) admin_api.oauth2_plugins:insert({ route = { id = route3.id }, config = { scopes = { "email", "profile", "user.email" }, anonymous = anonymous.id, }, }) admin_api.plugins:insert { name = "key-auth", route = { id = route2.id }, config = { anonymous = anonymous.id, }, } admin_api.plugins:insert { name = "jwt", route = { id = route3.id }, config = { anonymous = anonymous.id, }, } keyauth = admin_api.keyauth_credentials:insert({ key = "Mouse", consumer = { id = user1.id }, }) jwt_secret = admin_api.jwt_secrets:insert({ consumer = { id = user1.id } }) admin_api.oauth2_credentials:insert { client_id = "clientid4567", client_secret = "secret4567", redirect_uris = { "http://google.com/kong" }, name = "testapp", consumer = { id = user2.id }, } proxy_client = helpers.proxy_client() end) lazy_teardown(function() if proxy_client then proxy_client:close() end end) describe("multiple auth without anonymous, logical AND", function() it("passes with all credentials provided", function() local token = provision_token("logical-and.com", { ["apikey"] = "Mouse"}, "clientid4567", "secret4567").access_token local res = assert(proxy_client:send { method = "GET", path = "/request", headers = { ["Host"] = "logical-and.com", ["apikey"] = "Mouse", -- we must provide the apikey again in the extra_headers, for the -- token endpoint, because that endpoint is also protected by the -- key-auth plugin. Otherwise getting the token simply fails. ["Authorization"] = "bearer " .. token, } }) assert.response(res).has.status(200) assert.request(res).has.no.header("x-anonymous-consumer") local id = assert.request(res).has.header("x-consumer-id") assert.not_equal(id, anonymous.id) assert(id == user1.id or id == user2.id) local client_id = assert.request(res).has.header("x-credential-identifier") assert.equal(keyauth.id, client_id) end) it("fails 401, with only the first credential provided", function() local res = assert(proxy_client:send { method = "GET", path = "/request", headers = { ["Host"] = "logical-and.com", ["apikey"] = "Mouse", } }) assert.response(res).has.status(401) end) it("fails 401, with only the second credential provided", function() local res = assert(proxy_client:send { method = "GET", path = "/request", headers = { ["Host"] = "logical-and.com", -- we must provide the apikey again in the extra_headers, for the -- token endpoint, because that endpoint is also protected by the -- key-auth plugin. Otherwise getting the token simply fails. ["Authorization"] = "bearer " .. provision_token("logical-and.com", {["apikey"] = "Mouse"}).access_token, } }) assert.response(res).has.status(401) end) it("fails 401, with no credential provided", function() local res = assert(proxy_client:send { method = "GET", path = "/request", headers = { ["Host"] = "logical-and.com", } }) assert.response(res).has.status(401) end) end) describe("multiple auth with anonymous, logical OR", function() it("passes with all credentials provided", function() local token = provision_token("logical-or.com", nil, "clientid4567", "secret4567").access_token local res = assert(proxy_client:send { method = "GET", path = "/request", headers = { ["Host"] = "logical-or.com", ["apikey"] = "Mouse", ["Authorization"] = "bearer " .. token, } }) assert.response(res).has.status(200) assert.request(res).has.no.header("x-anonymous-consumer") local id = assert.request(res).has.header("x-consumer-id") assert.not_equal(id, anonymous.id) assert(id == user1.id or id == user2.id) local client_id = assert.request(res).has.header("x-credential-identifier") assert.equal("clientid4567", client_id) end) it("passes with only the first credential provided (higher priority)", function() local res = assert(proxy_client:send { method = "GET", path = "/request", headers = { ["Host"] = "logical-or.com", ["apikey"] = "Mouse", ["X-Authenticated-Scope"] = "all-access", ["X-Authenticated-UserId"] = "admin", } }) assert.response(res).has.status(200) assert.request(res).has.no.header("x-anonymous-consumer") local id = assert.request(res).has.header("x-consumer-id") assert.not_equal(id, anonymous.id) assert.equal(user1.id, id) local client_id = assert.request(res).has.header("x-credential-identifier") assert.equal(keyauth.id, client_id) assert.request(res).has.no.header("x-authenticated-scope") assert.request(res).has.no.header("x-authenticated-userid") end) it("passes with only the first credential provided (lower priority)", function() PAYLOAD.iss = jwt_secret.key local jwt = jwt_encoder.encode(PAYLOAD, jwt_secret.secret) local authorization = "Bearer " .. jwt local res = assert(proxy_client:send { method = "GET", path = "/request", headers = { ["Host"] = "logical-or-jwt.com", ["Authorization"] = authorization, ["X-Authenticated-Scope"] = "all-access", ["X-Authenticated-UserId"] = "admin", } }) assert.response(res).has.status(200) assert.request(res).has.no.header("x-anonymous-consumer") local id = assert.request(res).has.header("x-consumer-id") assert.not_equal(id, anonymous.id) assert.equal(user1.id, id) local client_id = assert.request(res).has.header("x-credential-identifier") assert.equal(jwt_secret.key, client_id) assert.request(res).has.no.header("x-authenticated-scope") assert.request(res).has.no.header("x-authenticated-userid") end) it("passes with only the second credential provided", function() local token = provision_token("logical-or.com", nil, "clientid4567", "secret4567").access_token local res = assert(proxy_client:send { method = "GET", path = "/request", headers = { ["Host"] = "logical-or.com", ["Authorization"] = "bearer " .. token, } }) assert.response(res).has.status(200) assert.request(res).has.no.header("x-anonymous-consumer") local id = assert.request(res).has.header("x-consumer-id") assert.not_equal(id, anonymous.id) assert.equal(user2.id, id) local client_id = assert.request(res).has.header("x-credential-identifier") assert.equal("clientid4567", client_id) end) it("passes with no credential provided", function() local res = assert(proxy_client:send { method = "GET", path = "/request", headers = { ["Host"] = "logical-or.com", } }) assert.response(res).has.status(200) assert.request(res).has.header("x-anonymous-consumer") local id = assert.request(res).has.header("x-consumer-id") assert.equal(id, anonymous.id) assert.request(res).has.no.header("x-credential-identifier") end) end) end) describe("Plugin: oauth2 (ttl) with #"..strategy, function() lazy_setup(function() local route11 = assert(admin_api.routes:insert({ hosts = { "oauth2_21.com" }, protocols = { "http", "https" }, service = admin_api.services:insert(), })) admin_api.plugins:insert { name = "oauth2", route = { id = route11.id }, config = { enable_authorization_code = true, mandatory_scope = false, provision_key = "provision123", global_credentials = false, refresh_token_ttl = 2 } } local route12 = assert(admin_api.routes:insert({ hosts = { "oauth2_22.com" }, protocols = { "http", "https" }, service = admin_api.services:insert(), })) admin_api.plugins:insert { name = "oauth2", route = { id = route12.id }, config = { enable_authorization_code = true, mandatory_scope = false, provision_key = "provision123", global_credentials = false, refresh_token_ttl = 0 } } local consumer = admin_api.consumers:insert { username = "bobo" } admin_api.oauth2_credentials:insert { client_id = "clientid7890", client_secret = "secret7890", redirect_uris = { "http://google.com/kong" }, name = "testapp", consumer = { id = consumer.id }, } end) describe("refresh token", function() it("is deleted after defined TTL", function() local token = provision_token("oauth2_21.com", nil, "clientid7890", "secret7890") local token_entity = db.oauth2_tokens:select_by_access_token(token.access_token) assert.is_table(token_entity) local err helpers.wait_until(function() token_entity, err = db.oauth2_tokens:select_by_access_token(token.access_token) return token_entity == nil and err == nil end, 3) end) it("is not deleted when when TTL is 0 == never", function() local token = provision_token("oauth2_22.com", nil, "clientid7890", "secret7890") local token_entity = db.oauth2_tokens:select_by_access_token(token.access_token) assert.is_table(token_entity) ngx.sleep(2.2) token_entity = db.oauth2_tokens:select_by_access_token(token.access_token) assert.is_table(token_entity) end) end) end) describe("Plugin: oauth2 regressions", function() it("responds 401 when using global token against non-global plugin", function() -- Regression test for issue: -- https://github.com/Kong/kong/issues/4232 -- setup local route_token = assert(admin_api.routes:insert({ hosts = { "oauth2_regression_4232.com" }, protocols = { "http", "https" }, service = admin_api.services:insert(), })) admin_api.plugins:insert { name = "oauth2", route = { id = route_token.id }, config = { provision_key = "provision123", enable_authorization_code = true, global_credentials = true, } } local route_test = assert(admin_api.routes:insert({ hosts = { "oauth2_regression_4232_test.com" }, protocols = { "http", "https" }, service = admin_api.services:insert(), })) admin_api.plugins:insert { name = "oauth2", route = { id = route_test.id }, config = { enable_client_credentials = true, global_credentials = false, } } local consumer = admin_api.consumers:insert { username = "4232", } admin_api.oauth2_credentials:insert { client_id = "clientid_4232", client_secret = "secret_4232", redirect_uris = { "http://google.com/kong" }, name = "4232_app", consumer = { id = consumer.id }, } -- /setup local token = provision_token("oauth2_regression_4232.com", nil, "clientid_4232", "secret_4232") local proxy_ssl_client = helpers.proxy_ssl_client() local res = assert(proxy_ssl_client:send { method = "POST", path = "/anything", body = { access_token = token.access_token }, headers = { ["Host"] = "oauth2_regression_4232_test.com", ["Content-Type"] = "application/json" } }) local body = assert.res_status(401, res) local json = cjson.decode(body) assert.same({ error_description = "The access token is global, but the current " .. "plugin is configured without 'global_credentials'", error = "invalid_token", }, json) end) end) end) end