kong/spec/03-plugins/25-oauth2/02-api_spec.lua (914 lines of code) (raw):
local cjson = require "cjson"
local helpers = require "spec.helpers"
local admin_api = require "spec.fixtures.admin_api"
local secret = require "kong.plugins.oauth2.secret"
for _, strategy in helpers.each_strategy() do
describe("Plugin: oauth (API) [#" .. strategy .. "]", function()
local admin_client
local db
lazy_setup(function()
local _
_, db = helpers.get_db_utils(strategy, {
"routes",
"services",
"consumers",
"plugins",
"oauth2_tokens",
"oauth2_authorization_codes",
"oauth2_credentials",
})
helpers.prepare_prefix()
assert(helpers.start_kong({
database = strategy,
nginx_conf = "spec/fixtures/custom_nginx.template",
}))
admin_client = helpers.admin_client()
end)
lazy_teardown(function()
if admin_client then admin_client:close() end
assert(helpers.stop_kong())
helpers.clean_prefix()
end)
describe("/consumers/:consumer/oauth2/", function()
local consumer
local service
lazy_setup(function()
service = admin_api.services:insert({ host = "oauth2_token.com" })
consumer = admin_api.consumers:insert({ username = "bob" })
admin_api.consumers:insert({ username = "sally" })
end)
lazy_teardown(function()
admin_api.consumers:remove(consumer)
admin_api.services:remove(service)
end)
describe("POST", function()
it("creates a oauth2 credential", function()
local res = assert(admin_client:send {
method = "POST",
path = "/consumers/bob/oauth2",
body = {
name = "Test APP",
redirect_uris = { "http://google.com/" },
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = cjson.decode(assert.res_status(201, res))
assert.equal(consumer.id, body.consumer.id)
assert.equal("Test APP", body.name)
assert.same({ "http://google.com/" }, body.redirect_uris)
res = assert(admin_client:send {
method = "POST",
path = "/consumers/bob/oauth2",
body = {
name = "Test APP",
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = cjson.decode(assert.res_status(201, res))
assert.equal(consumer.id, body.consumer.id)
assert.equal("Test APP", body.name)
assert.same(ngx.null, body.redirect_uris)
end)
it("creates an oauth2 credential with tags", function()
local res = assert(admin_client:send {
method = "POST",
path = "/consumers/bob/oauth2",
body = {
name = "Tags APP",
redirect_uris = { "http://example.com/" },
tags = { "tag1", "tag2" },
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.res_status(201, res)
local json = cjson.decode(body)
assert.equal(consumer.id, json.consumer.id)
assert.equal("tag1", json.tags[1])
assert.equal("tag2", json.tags[2])
end)
it("creates a oauth2 credential with multiple redirect_uris", function()
local res = assert(admin_client:send {
method = "POST",
path = "/consumers/bob/oauth2",
body = {
name = "Test APP",
redirect_uris = { "http://google.com/", "http://google.org/" },
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = cjson.decode(assert.res_status(201, res))
assert.equal(consumer.id, body.consumer.id)
assert.equal("Test APP", body.name)
assert.same({ "http://google.com/", "http://google.org/" }, body.redirect_uris)
end)
it("creates multiple oauth2 credentials with the same client_secret", function()
local res = assert(admin_client:send {
method = "POST",
path = "/consumers/bob/oauth2",
body = {
name = "Test APP",
redirect_uris = { "http://google.com/" },
client_secret = "secret123",
},
headers = {
["Content-Type"] = "application/json"
}
})
assert.res_status(201, res)
res = assert(admin_client:send {
method = "POST",
path = "/consumers/sally/oauth2",
body = {
name = "Test APP",
redirect_uris = { "http://google.com/" },
client_secret = "secret123",
},
headers = {
["Content-Type"] = "application/json"
}
})
assert.res_status(201, res)
end)
it("creates oauth2 credential with a hashed auto-generated client_secret", function()
-- this is quite useless as nobody knows the client_secret
local res = assert(admin_client:send {
method = "POST",
path = "/consumers/bob/oauth2",
body = {
name = "Test APP",
redirect_uris = { "http://google.com/" },
hash_secret = true,
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = cjson.decode(assert.res_status(201, res))
assert.equal(true, body.hash_secret)
assert.equal(false, secret.needs_rehash(body.client_secret))
end)
it("creates oauth2 credential with a hashed client_secret", function()
local res = assert(admin_client:send {
method = "POST",
path = "/consumers/bob/oauth2",
body = {
name = "Test APP",
redirect_uris = { "http://google.com/" },
client_secret = "test",
hash_secret = true,
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = cjson.decode(assert.res_status(201, res))
assert.equal(true, body.hash_secret)
assert.equal(false, secret.needs_rehash(body.client_secret))
assert.equal(true, secret.verify("test", body.client_secret))
assert.equal(false, secret.verify("invalid", body.client_secret))
end)
it("creates oauth2 credential without hashing the auto-generated client_secret", function()
local res = assert(admin_client:send {
method = "POST",
path = "/consumers/bob/oauth2",
body = {
name = "Test APP",
redirect_uris = { "http://google.com/" },
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = cjson.decode(assert.res_status(201, res))
assert.equal(false, body.hash_secret)
assert.equal(true, secret.needs_rehash(body.client_secret))
end)
it("creates oauth2 credential without hashing the client_secret", function()
local res = assert(admin_client:send {
method = "POST",
path = "/consumers/bob/oauth2",
body = {
name = "Test APP",
redirect_uris = { "http://google.com/" },
client_secret = "test",
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = cjson.decode(assert.res_status(201, res))
assert.equal(false, body.hash_secret)
assert.equal(true, secret.needs_rehash(body.client_secret))
assert.equal(false, secret.verify("test", body.client_secret))
assert.equal(false, secret.verify("invalid", body.client_secret))
end)
describe("errors", function()
it("returns bad request", function()
local res = assert(admin_client:send {
method = "POST",
path = "/consumers/bob/oauth2",
body = {},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.res_status(400, res)
local json = cjson.decode(body)
assert.same({ name = "required field missing" }, json.fields)
end)
it("returns bad request with invalid redirect_uris", function()
local res = assert(admin_client:send {
method = "POST",
path = "/consumers/bob/oauth2",
body = {
name = "Test APP",
redirect_uris = { "not-valid" }
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.res_status(400, res)
local json = cjson.decode(body)
assert.same({ redirect_uris = { "cannot parse 'not-valid'" } }, json.fields)
local res = assert(admin_client:send {
method = "POST",
path = "/consumers/bob/oauth2",
body = {
name = "Test APP",
redirect_uris = { "http://test.com/#with-fragment" },
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.res_status(400, res)
local json = cjson.decode(body)
assert.same({ redirect_uris = { "fragment not allowed in 'http://test.com/#with-fragment'" } }, json.fields)
local res = assert(admin_client:send {
method = "POST",
path = "/consumers/bob/oauth2",
body = {
name = "Test APP",
redirect_uris = {"http://valid.com", "not-valid"}
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.res_status(400, res)
local json = cjson.decode(body)
assert.same({ redirect_uris = { ngx.null, "cannot parse 'not-valid'" } }, json.fields)
local res = assert(admin_client:send {
method = "POST",
path = "/consumers/bob/oauth2",
body = {
name = "Test APP",
redirect_uris = {"http://valid.com", "http://test.com/#with-fragment"}
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.res_status(400, res)
local json = cjson.decode(body)
assert.same({ redirect_uris = {
ngx.null,
"fragment not allowed in 'http://test.com/#with-fragment'"
} }, json.fields)
end)
end)
end)
describe("PUT", function()
it("creates an oauth2 credential", function()
local res = assert(admin_client:send {
method = "PUT",
path = "/consumers/bob/oauth2/client_one",
body = {
name = "Test APP",
redirect_uris = { "http://google.com/" },
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = cjson.decode(assert.res_status(200, res))
assert.equal(consumer.id, body.consumer.id)
assert.equal("Test APP", body.name)
assert.equal("client_one", body.client_id)
assert.same({ "http://google.com/" }, body.redirect_uris)
local res = assert(admin_client:send {
method = "PUT",
path = "/consumers/bob/oauth2/client_one",
body = {
name = "Test APP",
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = cjson.decode(assert.res_status(200, res))
assert.equal(consumer.id, body.consumer.id)
assert.equal("Test APP", body.name)
assert.equal("client_one", body.client_id)
assert.same(ngx.null, body.redirect_uris)
end)
describe("errors", function()
it("returns bad request", function()
local res = assert(admin_client:send {
method = "PUT",
path = "/consumers/bob/oauth2/client_two",
body = {},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.res_status(400, res)
local json = cjson.decode(body)
assert.same({ name = "required field missing" }, json.fields)
end)
end)
end)
describe("GET", function()
local consumer = admin_api.consumers:insert({
username = "get_test",
})
local credentials = {}
lazy_setup(function()
for i = 1, 3 do
credentials[i] = admin_api.oauth2_credentials:insert {
name = "app" .. i,
redirect_uris = { helpers.mock_upstream_ssl_url },
consumer = { id = consumer.id },
}
end
end)
lazy_teardown(function()
for _, credential in ipairs(credentials) do
admin_api.oauth2_credentials:remove(credential)
end
end)
it("retrieves the first page", function()
local res = assert(admin_client:send {
method = "GET",
path = "/consumers/get_test/oauth2"
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.is_table(json.data)
assert.equal(3, #json.data)
end)
end)
end)
describe("/consumers/:consumer/oauth2/:id", function()
local credential
local consumer
local service
lazy_setup(function()
service = admin_api.services:insert({ host = "oauth2_token.com" })
consumer = admin_api.consumers:insert({ username = "bob" })
end)
lazy_teardown(function()
admin_api.consumers:remove(consumer)
admin_api.services:remove(service)
end)
before_each(function()
credential = admin_api.oauth2_credentials:insert {
name = "test app",
redirect_uris = { helpers.mock_upstream_ssl_url },
consumer = { id = consumer.id },
}
end)
after_each(function()
admin_api.oauth2_credentials:remove(credential)
end)
describe("GET", function()
it("retrieves oauth2 credential by id", function()
local res = assert(admin_client:send {
method = "GET",
path = "/consumers/bob/oauth2/" .. credential.id
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.equal(credential.id, json.id)
end)
it("retrieves oauth2 credential by client id", function()
local res = assert(admin_client:send {
method = "GET",
path = "/consumers/bob/oauth2/" .. credential.client_id
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.equal(credential.id, json.id)
end)
it("retrieves credential by id only if the credential belongs to the specified consumer", function()
local alice = admin_api.consumers:insert {
username = "alice"
}
finally(function()
admin_api.consumers:remove(alice)
end)
local res = assert(admin_client:send {
method = "GET",
path = "/consumers/bob/oauth2/" .. credential.id
})
assert.res_status(200, res)
res = assert(admin_client:send {
method = "GET",
path = "/consumers/alice/oauth2/" .. credential.id
})
assert.res_status(404, res)
end)
it("retrieves credential by clientid only if the credential belongs to the specified consumer", function()
local res = assert(admin_client:send {
method = "GET",
path = "/consumers/bob/oauth2/" .. credential.client_id
})
assert.res_status(200, res)
res = assert(admin_client:send {
method = "GET",
path = "/consumers/alice/oauth2/" .. credential.client_id
})
assert.res_status(404, res)
end)
end)
describe("PATCH", function()
it("updates a credential by id", function()
local previous_name = credential.name
local res = assert(admin_client:send {
method = "PATCH",
path = "/consumers/bob/oauth2/" .. credential.id,
body = {
name = "4321"
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.not_equal(previous_name, json.name)
end)
it("updates a credential by client id", function()
local previous_name = credential.name
local res = assert(admin_client:send {
method = "PATCH",
path = "/consumers/bob/oauth2/" .. credential.client_id,
body = {
name = "4321UDP"
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.not_equal(previous_name, json.name)
end)
describe("errors", function()
it("handles invalid input", function()
local res = assert(admin_client:send {
method = "PATCH",
path = "/consumers/bob/oauth2/" .. credential.id,
body = {
redirect_uris = { "not-valid" },
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.res_status(400, res)
local json = cjson.decode(body)
assert.same({ redirect_uris = { "cannot parse 'not-valid'" } }, json.fields)
end)
it("updating client_secret requires hash_secret", function()
local res = assert(admin_client:send {
method = "PATCH",
path = "/consumers/bob/oauth2/" .. credential.id,
body = {
client_secret = "test",
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.res_status(400, res)
local json = cjson.decode(body)
assert.same({
["@entity"] = {
"all or none of these fields must be set: 'hash_secret', 'client_secret'"
}
}, json.fields)
end)
it("updating hash_secret requires client_secret", function()
local res = assert(admin_client:send {
method = "PATCH",
path = "/consumers/bob/oauth2/" .. credential.id,
body = {
hash_secret = true,
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.res_status(400, res)
local json = cjson.decode(body)
assert.same({
["@entity"] = {
"all or none of these fields must be set: 'hash_secret', 'client_secret'"
}
}, json.fields)
end)
end)
end)
describe("DELETE", function()
it("deletes a credential", function()
local res = assert(admin_client:send {
method = "DELETE",
path = "/consumers/bob/oauth2/" .. credential.id,
})
assert.res_status(204, res)
end)
describe("errors", function()
it("returns 400 on invalid input", function()
local res = assert(admin_client:send {
method = "DELETE",
path = "/consumers/bob/oauth2/blah"
})
assert.res_status(404, res)
end)
it("returns 404 if not found", function()
local res = assert(admin_client:send {
method = "DELETE",
path = "/consumers/bob/oauth2/00000000-0000-0000-0000-000000000000"
})
assert.res_status(404, res)
end)
end)
end)
end)
describe("/oauth2", function()
describe("POST", function()
local consumer
local service
lazy_setup(function()
service = admin_api.services:insert({ host = "oauth2_token.com" })
consumer = admin_api.consumers:insert({ username = "bob" })
end)
lazy_teardown(function()
admin_api.consumers:remove(consumer)
admin_api.services:remove(service)
end)
it("does not create oauth2 credential when missing consumer", function()
local res = assert(admin_client:send {
method = "POST",
path = "/oauth2",
body = {
name = "test",
redirect_uris = { "http://localhost/" },
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.res_status(400, res)
local json = cjson.decode(body)
assert.same("schema violation (consumer: required field missing)", json.message)
end)
it("creates oauth2 credential", function()
local res = assert(admin_client:send {
method = "POST",
path = "/oauth2",
body = {
name = "test",
redirect_uris = { "http://localhost/" },
consumer = {
id = consumer.id
}
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.res_status(201, res)
local json = cjson.decode(body)
assert.equal("test", json.name)
end)
end)
end)
describe("/oauth2/:client_id_or_id", function()
describe("PUT", function()
local consumer
local service
lazy_setup(function()
service = admin_api.services:insert({ host = "oauth2_token.com" })
consumer = admin_api.consumers:insert({ username = "bob" })
end)
lazy_teardown(function()
admin_api.consumers:remove(consumer)
admin_api.services:remove(service)
end)
it("does not create oauth2 credential when missing consumer", function()
local res = assert(admin_client:send {
method = "PUT",
path = "/oauth2/client-1",
body = {
name = "test",
redirect_uris = { "http://localhost/" },
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.res_status(400, res)
local json = cjson.decode(body)
assert.same("schema violation (consumer: required field missing)", json.message)
end)
it("creates oauth2 credential", function()
local res = assert(admin_client:send {
method = "PUT",
path = "/oauth2/client-1",
body = {
name = "test",
redirect_uris = { "http://localhost/" },
consumer = {
id = consumer.id
}
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.equal("client-1", json.client_id)
assert.equal("test", json.name)
end)
end)
end)
describe("/oauth2_tokens/", function()
describe("POST", function()
local oauth2_credential
local consumer
local service
lazy_setup(function()
service = admin_api.services:insert({ host = "oauth2_token.com" })
consumer = admin_api.consumers:insert({ username = "bob" })
oauth2_credential = admin_api.oauth2_credentials:insert {
name = "Test APP",
redirect_uris = { helpers.mock_upstream_ssl_url },
consumer = { id = consumer.id },
}
end)
lazy_teardown(function()
admin_api.consumers:remove(consumer)
admin_api.services:remove(service)
end)
it("creates a oauth2 token", function()
local res = assert(admin_client:send {
method = "POST",
path = "/oauth2_tokens",
body = {
credential = { id = oauth2_credential.id },
service = { id = service.id },
expires_in = 10
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = cjson.decode(assert.res_status(201, res))
assert.equal(oauth2_credential.id, body.credential.id)
assert.equal(10, body.expires_in)
assert.truthy(body.access_token)
assert.truthy(body.service.id)
assert.same(ngx.null, body.refresh_token)
assert.equal("bearer", body.token_type)
end)
describe("errors", function()
it("returns bad request", function()
local res = assert(admin_client:send {
method = "POST",
path = "/oauth2_tokens",
body = {},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.res_status(400, res)
local json = cjson.decode(body)
assert.same({
expires_in = "required field missing",
credential = 'required field missing',
}, json.fields)
end)
end)
end)
describe("GET", function()
local oauth2_credential
local consumer
local service
lazy_setup(function()
service = admin_api.services:insert({ host = "oauth2_token.com" })
consumer = admin_api.consumers:insert({ username = "bob" })
oauth2_credential = admin_api.oauth2_credentials:insert {
name = "Test APP",
redirect_uris = { helpers.mock_upstream_ssl_url },
consumer = { id = consumer.id },
}
end)
lazy_teardown(function()
admin_api.consumers:remove(consumer)
admin_api.services:remove(service)
end)
it("retrieves the first page", function()
for i = 1, 3 do
admin_api.oauth2_tokens:insert {
credential = { id = oauth2_credential.id },
service = { id = service.id },
expires_in = 10
}
end
local res = assert(admin_client:send {
method = "GET",
path = "/oauth2_tokens"
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.is_table(json.data)
assert.equal(3, #json.data)
end)
end)
describe("/oauth2_tokens/:id", function()
local oauth2_credential
local consumer
local service
lazy_setup(function()
service = admin_api.services:insert({ host = "oauth2_token.com" })
consumer = admin_api.consumers:insert({ username = "bob" })
oauth2_credential = admin_api.oauth2_credentials:insert {
name = "Test APP",
redirect_uris = { helpers.mock_upstream_ssl_url },
consumer = { id = consumer.id },
}
end)
lazy_teardown(function()
admin_api.consumers:remove(consumer)
admin_api.services:remove(service)
end)
local token
before_each(function()
token = db.oauth2_tokens:insert {
credential = { id = oauth2_credential.id },
service = { id = service.id },
expires_in = 10
}
end)
after_each(function()
admin_api.oauth2_tokens:remove(token)
end)
describe("GET", function()
it("retrieves oauth2 token by id", function()
local res = assert(admin_client:send {
method = "GET",
path = "/oauth2_tokens/" .. token.id
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.equal(token.id, json.id)
end)
it("retrieves oauth2 token by access_token", function()
local res = assert(admin_client:send {
method = "GET",
path = "/oauth2_tokens/" .. token.access_token
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.equal(token.id, json.id)
end)
end)
describe("PUT", function()
it("creates an oauth2 credential", function()
local res = assert(admin_client:send {
method = "PUT",
path = "/oauth2_tokens/foobar",
body = {
credential = { id = oauth2_credential.id },
service = { id = service.id },
expires_in = 10
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = cjson.decode(assert.res_status(200, res))
assert.equal(oauth2_credential.id, body.credential.id)
assert.equal(10, body.expires_in)
assert.equal("foobar", body.access_token)
assert.equal(ngx.null, body.refresh_token)
assert.equal("bearer", body.token_type)
end)
describe("errors", function()
it("returns bad request", function()
local res = assert(admin_client:send {
method = "PUT",
path = "/oauth2_tokens/foobar",
body = {},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.res_status(400, res)
local json = cjson.decode(body)
assert.same({
expires_in = "required field missing",
credential = 'required field missing',
}, json.fields)
end)
end)
end)
describe("PATCH", function()
it("updates a token by id", function()
local previous_expires_in = token.expires_in
local res = assert(admin_client:send {
method = "PATCH",
path = "/oauth2_tokens/" .. token.id,
body = {
expires_in = 20
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.not_equal(previous_expires_in, json.expires_in)
end)
it("updates a token by access_token", function()
local previous_expires_in = token.expires_in
local res = assert(admin_client:send {
method = "PATCH",
path = "/oauth2_tokens/" .. token.access_token,
body = {
expires_in = 400
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.not_equal(previous_expires_in, json.expires_in)
end)
describe("errors", function()
it("handles invalid input", function()
local res = assert(admin_client:send {
method = "PATCH",
path = "/oauth2_tokens/" .. token.id,
body = {
expires_in = "hello"
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.res_status(400, res)
local json = cjson.decode(body)
assert.same({ expires_in = "expected an integer" }, json.fields)
end)
end)
end)
describe("DELETE", function()
it("deletes a token", function()
local res = assert(admin_client:send {
method = "DELETE",
path = "/oauth2_tokens/" .. token.id,
})
assert.res_status(204, res)
end)
describe("errors", function()
it("returns 204 on inexisting tokens", function()
local res = assert(admin_client:send {
method = "DELETE",
path = "/oauth2_tokens/blah"
})
assert.res_status(204, res)
local res = assert(admin_client:send {
method = "DELETE",
path = "/oauth2_tokens/00000000-0000-0000-0000-000000000000"
})
assert.res_status(204, res)
end)
end)
end)
end)
end)
end)
end