From f7da79057ce29c7d1f6d90f4bc160cc3d9c8611f Mon Sep 17 00:00:00 2001 From: thefosk Date: Mon, 27 Jul 2015 15:52:27 -0700 Subject: [PATCH] Closes #430 and also fixes a problem when starting dnsmasq --- kong/cli/utils/dnsmasq.lua | 17 +++++- kong/plugins/basicauth/access.lua | 8 ++- kong/plugins/oauth2/access.lua | 89 +++++++++++++++++++++++------ kong/plugins/oauth2/schema.lua | 2 + spec/plugins/oauth2/access_spec.lua | 63 ++++++++++++++++++-- 5 files changed, 151 insertions(+), 28 deletions(-) diff --git a/kong/cli/utils/dnsmasq.lua b/kong/cli/utils/dnsmasq.lua index 53b695820d9..b06b17be7b8 100644 --- a/kong/cli/utils/dnsmasq.lua +++ b/kong/cli/utils/dnsmasq.lua @@ -14,8 +14,19 @@ function _M.stop(kong_config) end function _M.start(kong_config) - local cmd = IO.cmd_exists("dnsmasq") and "dnsmasq" or - (IO.cmd_exists("/usr/local/sbin/dnsmasq") and "/usr/local/sbin/dnsmasq" or nil) -- On OS X dnsmasq is at /usr/local/sbin/ + local cmd = IO.cmd_exists("dnsmasq") and "dnsmasq" + + if not cmd then -- Load dnsmasq given the PATH settings + local env_path = (os.getenv("PATH")..":" or "").."/usr/local/sbin:/usr/sbin" -- Also check in default paths + local paths = stringy.split(env_path, ":") + for _, path in ipairs(paths) do + if IO.file_exists(path..(stringy.endswith(path, "/") and "" or "/").."dnsmasq") then + cmd = path.."/dnsmasq" + break + end + end + end + if not cmd then cutils.logger:error_exit("Can't find dnsmasq") end @@ -26,7 +37,7 @@ function _M.start(kong_config) if code ~= 0 then cutils.logger:error_exit(res) else - cutils.logger:info("dnsmasq started") + cutils.logger:info("dnsmasq started ("..cmd..")") end end diff --git a/kong/plugins/basicauth/access.lua b/kong/plugins/basicauth/access.lua index ae7e6a0c439..8146aafd007 100644 --- a/kong/plugins/basicauth/access.lua +++ b/kong/plugins/basicauth/access.lua @@ -37,9 +37,11 @@ local function retrieve_credentials(request, conf) if m and table.getn(m) > 0 then local decoded_basic = ngx.decode_base64(m[1]) - local basic_parts = stringy.split(decoded_basic, ":") - username = basic_parts[1] - password = basic_parts[2] + if decoded_basic then + local basic_parts = stringy.split(decoded_basic, ":") + username = basic_parts[1] + password = basic_parts[2] + end end else ngx.ctx.stop_phases = true diff --git a/kong/plugins/oauth2/access.lua b/kong/plugins/oauth2/access.lua index 8fc9dd271da..fc68311b569 100644 --- a/kong/plugins/oauth2/access.lua +++ b/kong/plugins/oauth2/access.lua @@ -20,6 +20,7 @@ local REDIRECT_URI = "redirect_uri" local ACCESS_TOKEN = "access_token" local GRANT_TYPE = "grant_type" local GRANT_AUTHORIZATION_CODE = "authorization_code" +local GRANT_CLIENT_CREDENTIALS = "client_credentials" local GRANT_REFRESH_TOKEN = "refresh_token" local ERROR = "error" local AUTHENTICATED_USERID = "authenticated_userid" @@ -74,6 +75,24 @@ local function retrieve_parameters() return utils.table_merge(ngx.req.get_uri_args(), ngx.req.get_post_args()) end +local function retrieve_scopes(parameters, conf) + local scope = parameters[SCOPE] + local scopes = {} + if conf.scopes and scope then + for v in scope:gmatch("%w+") do + if not utils.table_contains(conf.scopes, v) then + return false, {[ERROR] = "invalid_scope", error_description = "\""..v.."\" is an invalid "..SCOPE} + else + table.insert(scopes, v) + end + end + elseif not scope and conf.mandatory_scope then + return false, {[ERROR] = "invalid_scope", error_description = "You must specify a "..SCOPE} + end + + return true, scopes +end + local function authorize(conf) local response_params = {} @@ -89,24 +108,14 @@ local function authorize(conf) else local response_type = parameters[RESPONSE_TYPE] -- Check response_type - if not (response_type == CODE or (conf.enable_implicit_grant and response_type == TOKEN)) then -- Authorization Code Grant (http://tools.ietf.org/html/rfc6749#section-4.1.1) + if not ((response_type == CODE and conf.enable_authorization_code) or (conf.enable_implicit_grant and response_type == TOKEN)) then -- Authorization Code Grant (http://tools.ietf.org/html/rfc6749#section-4.1.1) response_params = {[ERROR] = "unsupported_response_type", error_description = "Invalid "..RESPONSE_TYPE} end -- Check scopes - local scope = parameters[SCOPE] - local scopes = {} - if conf.scopes and scope then - for v in scope:gmatch("%w+") do - if not utils.table_contains(conf.scopes, v) then - response_params = {[ERROR] = "invalid_scope", error_description = "\""..v.."\" is an invalid "..SCOPE} - break - else - table.insert(scopes, v) - end - end - elseif not scope and conf.mandatory_scope then - response_params = {[ERROR] = "invalid_scope", error_description = "You must specify a "..SCOPE} + local ok, scopes = retrieve_scopes(parameters, conf) + if not ok then + response_params = scopes -- If it's not ok, then this is the error message end -- Check client_id and redirect_uri @@ -154,6 +163,41 @@ local function authorize(conf) }) end +local function retrieve_client_credentials(parameters) + local client_id, client_secret + local authorization_header = ngx.req.get_headers()["authorization"] + if parameters[CLIENT_ID] then + client_id = parameters[CLIENT_ID] + client_secret = parameters[CLIENT_SECRET] + elseif authorization_header then + local iterator, iter_err = ngx.re.gmatch(authorization_header, "\\s*[Bb]asic\\s*(.+)") + if not iterator then + ngx.log(ngx.ERR, iter_err) + return + end + + local m, err = iterator() + if err then + ngx.log(ngx.ERR, err) + return + end + + if m and table.getn(m) > 0 then + local decoded_basic = ngx.decode_base64(m[1]) + if decoded_basic then + local basic_parts = stringy.split(decoded_basic, ":") + client_id = basic_parts[1] + client_secret = basic_parts[2] + + print(client_id) + print(client_secret) + end + end + end + + return client_id, client_secret +end + local function issue_token(conf) local response_params = {} @@ -161,20 +205,21 @@ local function issue_token(conf) local state = parameters[STATE] local grant_type = parameters[GRANT_TYPE] - if not (grant_type == GRANT_AUTHORIZATION_CODE or grant_type == GRANT_REFRESH_TOKEN) then + if not (grant_type == GRANT_AUTHORIZATION_CODE or grant_type == GRANT_REFRESH_TOKEN or (conf.enable_client_credentials and grant_type == GRANT_CLIENT_CREDENTIALS)) then response_params = {[ERROR] = "invalid_request", error_description = "Invalid "..GRANT_TYPE} end + local client_id, client_secret = retrieve_client_credentials(parameters) + -- Check client_id and redirect_uri - local redirect_uri, client = get_redirect_uri(parameters[CLIENT_ID]) + local redirect_uri, client = get_redirect_uri(client_id) if not redirect_uri then response_params = {[ERROR] = "invalid_request", error_description = "Invalid "..CLIENT_ID} elseif parameters[REDIRECT_URI] and parameters[REDIRECT_URI] ~= redirect_uri then response_params = {[ERROR] = "invalid_request", error_description = "Invalid "..REDIRECT_URI.." that does not match with the one created with the application"} end - local client_secret = parameters[CLIENT_SECRET] - if not client_secret or (client and client_secret ~= client.client_secret) then + if client and client.client_secret ~= client_secret then response_params = {[ERROR] = "invalid_request", error_description = "Invalid "..CLIENT_SECRET} end @@ -187,6 +232,14 @@ local function issue_token(conf) else response_params = generate_token(conf, client, authorization_code.authenticated_userid, authorization_code.scope, state) end + elseif grant_type == GRANT_CLIENT_CREDENTIALS then + -- Check scopes + local ok, scopes = retrieve_scopes(parameters, conf) + if not ok then + response_params = scopes -- If it's not ok, then this is the error message + else + response_params = generate_token(conf, client, nil, table.concat(scopes, " "), state) + end elseif grant_type == GRANT_REFRESH_TOKEN then local refresh_token = parameters[REFRESH_TOKEN] local token = refresh_token and dao.oauth2_tokens:find_by_keys({refresh_token = refresh_token})[1] or nil diff --git a/kong/plugins/oauth2/schema.lua b/kong/plugins/oauth2/schema.lua index 2e65deb3cb6..a4cf607cd58 100644 --- a/kong/plugins/oauth2/schema.lua +++ b/kong/plugins/oauth2/schema.lua @@ -22,7 +22,9 @@ return { mandatory_scope = { required = true, type = "boolean", default = false, func = check_mandatory_scope }, provision_key = { required = false, unique = true, type = "string", func = generate_if_missing }, token_expiration = { required = true, type = "number", default = 7200 }, + enable_authorization_code = { required = true, type = "boolean", default = true }, enable_implicit_grant = { required = true, type = "boolean", default = false }, + enable_client_credentials = { required = true, type = "boolean", default = false }, hide_credentials = { type = "boolean", default = false } } } diff --git a/spec/plugins/oauth2/access_spec.lua b/spec/plugins/oauth2/access_spec.lua index 426e0c30c7d..f18f8f67e89 100644 --- a/spec/plugins/oauth2/access_spec.lua +++ b/spec/plugins/oauth2/access_spec.lua @@ -41,7 +41,8 @@ describe("Authentication Plugin", function() api = { { name = "tests oauth2", public_dns = "oauth2.com", target_url = "http://mockbin.com" }, { name = "tests oauth2 with path", public_dns = "mockbin-path.com", target_url = "http://mockbin.com", path = "/somepath/" }, - { name = "tests oauth2 with hide credentials", public_dns = "oauth2_3.com", target_url = "http://mockbin.com" } + { name = "tests oauth2 with hide credentials", public_dns = "oauth2_3.com", target_url = "http://mockbin.com" }, + { name = "tests oauth2 client credentials", public_dns = "oauth2_4.com", target_url = "http://mockbin.com" }, }, consumer = { { username = "auth_tests_consumer" } @@ -49,7 +50,8 @@ describe("Authentication Plugin", function() plugin_configuration = { { name = "oauth2", value = { scopes = { "email", "profile" }, mandatory_scope = true, provision_key = "provision123", token_expiration = 5, enable_implicit_grant = true }, __api = 1 }, { name = "oauth2", value = { scopes = { "email", "profile" }, mandatory_scope = true, provision_key = "provision123", token_expiration = 5, enable_implicit_grant = true }, __api = 2 }, - { name = "oauth2", value = { scopes = { "email", "profile" }, mandatory_scope = true, provision_key = "provision123", token_expiration = 5, enable_implicit_grant = true, hide_credentials = true }, __api = 3 } + { name = "oauth2", value = { scopes = { "email", "profile" }, mandatory_scope = true, provision_key = "provision123", token_expiration = 5, enable_implicit_grant = true, hide_credentials = true }, __api = 3 }, + { name = "oauth2", value = { scopes = { "email", "profile" }, mandatory_scope = true, provision_key = "provision123", token_expiration = 5, enable_client_credentials = true, enable_authorization_code = false }, __api = 4 }, }, oauth2_credential = { { client_id = "clientid123", client_secret = "secret123", redirect_uri = "http://google.com/kong", name="testapp", __consumer = 1 } @@ -252,6 +254,59 @@ describe("Authentication Plugin", function() end) end) + + describe("Client Credentials", function() + + it("should return an error when client_secret is not sent", function() + local response, status, headers = http_client.post(PROXY_URL.."/oauth2/token", { client_id = "clientid123", scope = "email", response_type = "token" }, {host = "oauth2_4.com"}) + local body = cjson.decode(response) + assert.are.equal(400, status) + assert.are.equal(2, utils.table_size(body)) + assert.are.equal("invalid_request", body.error) + assert.are.equal("Invalid client_secret", body.error_description) + end) + + it("should return an error when client_secret is not sent", function() + local response, status, headers = http_client.post(PROXY_URL.."/oauth2/token", { client_id = "clientid123", client_secret="secret123", scope = "email", response_type = "token" }, {host = "oauth2_4.com"}) + local body = cjson.decode(response) + assert.are.equal(400, status) + assert.are.equal(2, utils.table_size(body)) + assert.are.equal("invalid_request", body.error) + assert.are.equal("Invalid grant_type", body.error_description) + end) + + it("should return success", function() + local response, status, headers = http_client.post(PROXY_URL.."/oauth2/token", { client_id = "clientid123", client_secret="secret123", scope = "email", grant_type = "client_credentials" }, {host = "oauth2_4.com"}) + local body = cjson.decode(response) + assert.are.equal(200, status) + assert.are.equals(4, utils.table_size(body)) + assert.truthy(body.refresh_token) + assert.truthy(body.access_token) + assert.are.equal("bearer", body.token_type) + assert.are.equal(5, body.expires_in) + end) + + it("should return success with authorization header", function() + local response, status, headers = http_client.post(PROXY_URL.."/oauth2/token", { scope = "email", grant_type = "client_credentials" }, {host = "oauth2_4.com", authorization = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTIz"}) + local body = cjson.decode(response) + assert.are.equal(200, status) + assert.are.equals(4, utils.table_size(body)) + assert.truthy(body.refresh_token) + assert.truthy(body.access_token) + assert.are.equal("bearer", body.token_type) + assert.are.equal(5, body.expires_in) + end) + + it("should return an error with a wrong authorization header", function() + local response, status, headers = http_client.post(PROXY_URL.."/oauth2/token", { scope = "email", grant_type = "client_credentials" }, {host = "oauth2_4.com", authorization = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTI0"}) + local body = cjson.decode(response) + assert.are.equal(400, status) + assert.are.equal(2, utils.table_size(body)) + assert.are.equal("invalid_request", body.error) + assert.are.equal("Invalid client_secret", body.error_description) + end) + + end) end) describe("OAuth2 Access Token", function() @@ -262,7 +317,7 @@ describe("Authentication Plugin", function() assert.are.equal(400, status) assert.are.equal(2, utils.table_size(body)) assert.are.equal("invalid_request", body.error) - assert.are.equal("Invalid client_secret", body.error_description) + assert.are.equal("Invalid client_id", body.error_description) -- Checking headers assert.are.equal("no-store", headers["cache-control"]) @@ -277,7 +332,7 @@ describe("Authentication Plugin", function() assert.are.equal(400, status) assert.are.equal(2, utils.table_size(body)) assert.are.equal("invalid_request", body.error) - assert.are.equal("Invalid client_secret", body.error_description) + assert.are.equal("Invalid client_id", body.error_description) -- Checking headers assert.are.equal("no-store", headers["cache-control"])