Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(oauth2) security fixes #1409

Merged
merged 1 commit into from
Jul 21, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions kong/plugins/oauth2/access.lua
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ local function authorize(conf)
if not response_params[ERROR] then
if response_type == CODE then
local authorization_code, err = singletons.dao.oauth2_authorization_codes:insert({
credential_id = client.id,
authenticated_userid = parameters[AUTHENTICATED_USERID],
scope = table.concat(scopes, " ")
}, {ttl = 300})
Expand Down Expand Up @@ -285,8 +286,11 @@ local function issue_token(conf)
local authorization_code = code and singletons.dao.oauth2_authorization_codes:find_all({code = code})[1]
if not authorization_code then
response_params = {[ERROR] = "invalid_request", error_description = "Invalid "..CODE}
elseif authorization_code.credential_id ~= client.id then
response_params = {[ERROR] = "invalid_request", error_description = "Invalid "..CODE}
else
response_params = generate_token(conf, client, authorization_code.authenticated_userid, authorization_code.scope, state)
singletons.dao.oauth2_authorization_codes:delete({id=authorization_code.id}) -- Delete authorization code so it cannot be reused
end
elseif grant_type == GRANT_CLIENT_CREDENTIALS then
-- Only check the provision_key if the authenticated_userid is being set
Expand Down
1 change: 1 addition & 0 deletions kong/plugins/oauth2/daos.lua
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ local OAUTH2_AUTHORIZATION_CODES_SCHEMA = {
table = "oauth2_authorization_codes",
fields = {
id = { type = "id", dao_insert_value = true },
credential_id = { type = "id", required = true, foreign = "oauth2_credentials:id" },
code = { type = "string", required = false, unique = true, immutable = true, func = generate_if_missing },
authenticated_userid = { type = "string", required = false },
scope = { type = "string" },
Expand Down
10 changes: 10 additions & 0 deletions kong/plugins/oauth2/migrations/cassandra.lua
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,15 @@ return {
end
end
end
},
{
name = "2016-07-15-oauth2_code_credential_id",
up = [[
TRUNCATE oauth2_authorization_codes;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that not an issue for current production instances?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Authorization codes are different than access tokens. They are supposed to be consumed immediately and they expire after 5 minutes. Not a big deal.

ALTER TABLE oauth2_authorization_codes ADD credential_id uuid;
]],
down = [[
ALTER TABLE oauth2_authorization_codes DROP credential_id;
]]
}
}
10 changes: 10 additions & 0 deletions kong/plugins/oauth2/migrations/postgres.lua
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,15 @@ return {
DROP TABLE oauth2_authorization_codes;
DROP TABLE oauth2_tokens;
]]
},
{
name = "2016-07-15-oauth2_code_credential_id",
up = [[
DELETE FROM oauth2_authorization_codes;
ALTER TABLE oauth2_authorization_codes ADD COLUMN credential_id uuid REFERENCES oauth2_credentials (id) ON DELETE CASCADE;
]],
down = [[
ALTER TABLE oauth2_authorization_codes DROP COLUMN credential_id;
]]
}
}
64 changes: 62 additions & 2 deletions spec/03-plugins/oauth2/03-access_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ local helpers = require "spec.helpers"

describe("Plugin: oauth2 (access)", function()
local proxy_ssl_client, proxy_client
local client1
setup(function()
helpers.kill_all()
helpers.prepare_prefix()

local consumer = assert(helpers.dao.consumers:insert {
username = "bob"
})
assert(helpers.dao.oauth2_credentials:insert {
client1 = assert(helpers.dao.oauth2_credentials:insert {
client_id = "clientid123",
client_secret = "secret123",
redirect_uri = "http://google.com/kong",
Expand Down Expand Up @@ -142,6 +143,7 @@ describe("Plugin: oauth2 (access)", function()
end)

local function provision_code()
local proxy_ssl_client = helpers.proxy_ssl_client()
local res = assert(proxy_ssl_client:send {
method = "POST",
path = "/oauth2/authorize",
Expand All @@ -158,8 +160,8 @@ describe("Plugin: oauth2 (access)", function()
["Content-Type"] = "application/json"
}
})

local body = cjson.decode(assert.res_status(200, res))
proxy_ssl_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)
Expand Down Expand Up @@ -538,6 +540,7 @@ describe("Plugin: oauth2 (access)", function()
assert.are.equal(m[1], data[1].code)
assert.are.equal("userid123", data[1].authenticated_userid)
assert.are.equal("email", data[1].scope)
assert.are.equal(client1.id, data[1].credential_id)
end)
it("returns success with a dotted scope and store authenticated user properties", function()
local res = assert(proxy_ssl_client:send {
Expand Down Expand Up @@ -1311,6 +1314,63 @@ describe("Plugin: oauth2 (access)", function()
assert.are.equal("email", body.headers["x-authenticated-scope"])
assert.are.equal("userid123", body.headers["x-authenticated-userid"])
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)
assert.is_table(ngx.re.match(body, [[^\{"refresh_token":"[\w]{32,32}","token_type":"bearer","access_token":"[\w]{32,32}","expires_in":5\}$]]))

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)
assert.equal([[{"error":"invalid_request","error_description":"Invalid code"}]], body)
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)
assert.equal([[{"error":"invalid_request","error_description":"Invalid code"}]], body)
end)
end)

describe("Making a request", function()
Expand Down