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): fix a bug that refresh_token could be shared across instances #11342

Merged
merged 9 commits into from
Aug 10, 2023
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
`max_retry_delay` must now be `number`s greater than 0.001
(seconds).
[#10840](https://github.com/Kong/kong/pull/10840)
- For OAuth2 plugin, `scope` has been taken into account as a new criterion of the request validation. When refreshing token with `refresh_token`, the scopes associated with the `refresh_token` provided in the request must be same with or a subset of the scopes configured in the OAuth2 plugin instance hit by the request.
[#11342](https://github.com/Kong/kong/pull/11342)

### Additions

Expand Down
21 changes: 17 additions & 4 deletions kong/plugins/oauth2/access.lua
Original file line number Diff line number Diff line change
Expand Up @@ -757,15 +757,28 @@ local function issue_token(conf)
error_description = "Invalid " .. REFRESH_TOKEN
}

else
-- Check that the token belongs to the client application
if token.credential.id ~= client.id then
-- Check that the token belongs to the client application
elseif token.credential.id ~= client.id then
response_params = {
[ERROR] = "invalid_client",
error_description = "Invalid client authentication"
}

else
else
-- Check scopes
if token.scope then
for scope in token.scope:gmatch("%S+") do
if not table_contains(conf.scopes, scope) then
response_params = {
[ERROR] = "invalid_scope",
error_description = "Scope mismatch",
}
break
end
end
end

if not response_params[ERROR] then
response_params = generate_token(conf, kong.router.get_service(),
client,
token.authenticated_userid,
Expand Down
201 changes: 201 additions & 0 deletions spec/03-plugins/25-oauth2/03-access_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,10 @@ describe("Plugin: oauth2 [#" .. strategy .. "]", function()
local service15 = admin_api.services:insert()
local service16 = admin_api.services:insert()
local service17 = admin_api.services:insert()
local service18 = admin_api.services:insert()
local service19 = admin_api.services:insert()
local service20 = admin_api.services:insert()
local service21 = admin_api.services:insert()

local route1 = assert(admin_api.routes:insert({
hosts = { "oauth2.com" },
Expand Down Expand Up @@ -377,6 +381,30 @@ describe("Plugin: oauth2 [#" .. strategy .. "]", function()
service = service17,
}))

local route18 = assert(admin_api.routes:insert({
hosts = { "oauth2_18.com" },
protocols = { "http", "https" },
service = service18,
}))

local route19 = assert(admin_api.routes:insert({
hosts = { "oauth2_19.com" },
protocols = { "http", "https" },
service = service19,
}))

local route20 = assert(admin_api.routes:insert({
hosts = { "oauth2_20.com" },
protocols = { "http", "https" },
service = service20,
}))

local route21 = assert(admin_api.routes:insert({
hosts = { "oauth2_21.com" },
protocols = { "http", "https" },
service = service21,
}))

local service_grpc = assert(admin_api.services:insert {
name = "grpc",
url = helpers.grpcbin_url,
Expand Down Expand Up @@ -556,6 +584,38 @@ describe("Plugin: oauth2 [#" .. strategy .. "]", function()
global_credentials = true,
}
})

admin_api.oauth2_plugins:insert({
route = { id = route18.id },
config = {
scopes = { "scope18" },
global_credentials = true,
}
})

admin_api.oauth2_plugins:insert({
route = { id = route19.id },
config = {
scopes = { "scope19" },
global_credentials = true,
}
})

admin_api.oauth2_plugins:insert({
route = { id = route20.id },
config = {
scopes = { "scope20" },
global_credentials = true,
}
})

admin_api.oauth2_plugins:insert({
route = { id = route21.id },
config = {
scopes = { "scope20", "scope21" },
global_credentials = true,
}
})
end)

before_each(function ()
Expand Down Expand Up @@ -2905,6 +2965,147 @@ describe("Plugin: oauth2 [#" .. strategy .. "]", function()
})
assert.res_status(401, res)
end)

it("refreshing token fails when scope is mismatching", function ()
-- provision code
vm-001 marked this conversation as resolved.
Show resolved Hide resolved
local code, body, res
local request_client = helpers.proxy_ssl_client()
body = {
provision_key = "provision123",
client_id = "clientid123",
response_type = "code",
scope = "scope18",
state = "hello",
authenticated_userid = "userid123",
}
res = assert(request_client:send {
method = "POST",
path = "/oauth2/authorize",
body = body,
headers = kong.table.merge({
["Host"] = "oauth2_18.com",
["Content-Type"] = "application/json"
})
})
res = assert(cjson.decode(assert.res_status(200, res)))
if res.redirect_uri then
local iterator, err = ngx.re.gmatch(res.redirect_uri, "^http://google\\.com/kong\\?code=([\\w]{32,32})&state=hello$")
assert.is_nil(err)
local m, err = iterator()
assert.is_nil(err)
code = m[1]
end

-- provision token
body = {
code = code,
client_id = "clientid123",
client_secret = "secret123",
grant_type = "authorization_code",
redirect_uri = "http://google.com/kong",
}
res = assert(request_client:send {
method = "POST",
path = "/oauth2/token",
body = body,
headers = {
["Host"] = "oauth2_18.com",
["Content-Type"] = "application/json"
}
})
local token = assert(cjson.decode(assert.res_status(200, res)))

-- refresh token with mismatching scope
res = assert(request_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_19.com",
["Content-Type"] = "application/json"
}
})
res = assert(cjson.decode(assert.res_status(400, res)))
assert.same({
error = "invalid_scope",
error_description = "Scope mismatch"
}, res)
request_client:close()
end)

it("refreshing token succeeds when scope is a subset", function()
-- provision code
local code, body, res
local request_client = helpers.proxy_ssl_client()
body = {
provision_key = "provision123",
client_id = "clientid123",
response_type = "code",
scope = "scope20",
state = "hello",
authenticated_userid = "userid123",
}
res = assert(request_client:send {
method = "POST",
path = "/oauth2/authorize",
body = body,
headers = kong.table.merge({
["Host"] = "oauth2_20.com",
["Content-Type"] = "application/json"
})
})
res = assert(cjson.decode(assert.res_status(200, res)))
if res.redirect_uri then
local iterator, err = ngx.re.gmatch(res.redirect_uri, "^http://google\\.com/kong\\?code=([\\w]{32,32})&state=hello$")
assert.is_nil(err)
local m, err = iterator()
assert.is_nil(err)
code = m[1]
end

-- provision token
body = {
code = code,
client_id = "clientid123",
client_secret = "secret123",
grant_type = "authorization_code",
redirect_uri = "http://google.com/kong",
}
res = assert(request_client:send {
method = "POST",
path = "/oauth2/token",
body = body,
headers = {
["Host"] = "oauth2_20.com",
["Content-Type"] = "application/json"
}
})
local token = assert(cjson.decode(assert.res_status(200, res)))

-- refresh token with mismatching scope
local res = assert(request_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_21.com",
["Content-Type"] = "application/json"
}
})
assert(cjson.decode(assert.res_status(200, res)))
request_client:close()
end)
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add a test case that verifies the subsetting functionality.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

addressed


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")

Expand Down