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

Add token introspection feature as a policy. [THREESCALE-312] #619

Merged
merged 14 commits into from
Mar 1, 2018
Merged

Add token introspection feature as a policy. [THREESCALE-312] #619

merged 14 commits into from
Mar 1, 2018

Conversation

tmogi001
Copy link
Contributor

Hi team,

I created a new policy to enable RFC7662 OAuth2.0 Token Introspection.
This PR is a rework of the previous request (#435) as a policy.

And Also added sample configuration in the examples/policies directory.

local introspection_url = config.introspection_url
local client = config.client_id
local secret = config.client_secret
local credential = 'Basic ' .. ngx.encode_base64(table.concat({ client or '', secret or '' }, ':'))
Copy link
Contributor

Choose a reason for hiding this comment

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

This header can be moved to the _M.new, right?

if self.config.introspection_url then
local authorization = http_authorization.new(ngx.var.http_authorization)
local access_token = authorization.token
if not introspect_token(self, access_token) then
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be a bit nicer for introspect_token to return a table so this would look like:

if introspect_token(self, access_token).active then
  ...
else
  ... 
end

Or rename introspect_token to active_token because it actually returns true/false if the token is active or not.


if res.status == 200 then
local token_info = cjson.decode(res.body)
return token_info.active
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this active field defined in some RFC? Could we link to the relevant RFC section in a comment?

Choose a reason for hiding this comment

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

Came by to look at your PR review automation, stayed to help out w/ a little PR doc pointing. :-) Seems the appropriate RFC doc entry point is: https://tools.ietf.org/html/rfc7662#section-2.2 Where active is the only REQUIRED member of the returned JSON introspection result.

Copy link
Contributor

Choose a reason for hiding this comment

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

@reedstrm very nice. Thanks! This looks like the right doc.

@tmogi001 for cases where is RFC available I try to implement it as:
https://github.com/3scale/apicast/blob/5168008db2abb2e217d23f925599fd33da5248cb/gateway/src/resty/http_ng/cache_store.lua#L85-L110

Because the RFC is immutable the comments won't get out of sync and act as relevant context with links to the other parts of the RFC. In this case just the relevant parts to the code in question to document the requests and responses for example.

BTW it is pity OpenID Connect Discovery does not define token introspection endpoint. Some IDPs provide it (like Keycloak as token_introspection_endpoint), but I don't see it being part of the spec.

}
}

local res, err = self.http_client.post(introspection_url , { token = token, token_type_hint = 'access_token'}, opts)
Copy link
Contributor

@mikz mikz Feb 22, 2018

Choose a reason for hiding this comment

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

Would be good to link to the RFC section describing that introspection endpoint receives POST and what parameters.

Choose a reason for hiding this comment

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

https://tools.ietf.org/html/rfc7662#section-2.1 - only REQUIRED param is the token

local token_policy = require('apicast.policy.token_introspection').new(policy_config)
token_policy:access(context)

test_backend.verify_no_outstanding_expectations()
Copy link
Contributor

Choose a reason for hiding this comment

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

This line would be better in after_each block so it is not repeated in every test.

client_secret = "secret"
}

test_backend.expect{ url = introspection_url }.
Copy link
Contributor

Choose a reason for hiding this comment

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

It probably should expect right http method (POST) too ? And some other parameters like token_type_hint ? And possibly the headers?

local introspection_url = config.introspection_url
local client = config.client_id
local secret = config.client_secret
local credential = 'Basic ' .. ngx.encode_base64(table.concat({ client or '', secret or '' }, ':'))
Copy link
Contributor

Choose a reason for hiding this comment

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

@davidor I think we should provide a way to do this in http_ng constructor. Because it is done one several places IIRC.

backend = config.client,
options = {
header = { ['User-Agent'] = user_agent() },
ssl = { verify = resty_env.enabled('OPENSSL_VERIFY') }
Copy link
Contributor

Choose a reason for hiding this comment

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

@davidor we probably should provide a default value for these options/headers in the http_ng constructor to simplify its initialization.

@mikz
Copy link
Contributor

mikz commented Feb 22, 2018

@tmogi001 this is very good! 👍 🥇 My comments will be just nitpicks. This should be just minimal effort to be mergeable.
One of the things missing would be an integration test similar to other policy tests: https://github.com/3scale/apicast/blob/master/t/apicast-policy-cors.t
I think we could help with those a bit.

@reedstrm
Copy link

@mikz - As a random drive-by developer, with a group working on making our PR review policies more explicit (and automating as much as possible :-) - I landed here following examples of use of the CHANGELOG github app) I just wanted to say that I liked your review style here - not only do you review the code-as-presented, you call out issues w/ the shared testing infrastructure, to make future coding easier.

@mikz
Copy link
Contributor

mikz commented Feb 26, 2018

@reedstrm thank you very much! Means a lot.

Regarding the Changelog app (if you mean https://github.com/mikz/probot-changelog).
I'd say we are mostly happy. But because the changelog is changed in almost every PR it is hard when there is a lot of concurrent changes because they conflict. That means every PR then has to be resolved after there is another one merged. That can be quite frustrating. Maybe not as much when your testsuite runs in less than few minutes, but if you have to wait several minutes it is easy to forget.

I'm quite obsessed with automation and other approaches I was considering were:

But a changelog is very simple approach and can scale up to some level. Even some huge projects like Ruby on Rails still use one changelog file per component and it works.

@tmogi001
Copy link
Contributor Author

@mikz , @reedstrm Thank you for reviewing. I pushed fixed code and also added integration test case.

self.http_client = http_ng.new{
backend = config.client,
options = {
header = { ['User-Agent'] = user_agent() },
Copy link
Contributor

Choose a reason for hiding this comment

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

@tmogi001 any reason why not to add the 'Authorization' header here too?

--- Parameters for the token introspection endpoint.
-- https://tools.ietf.org/html/rfc7662#section-2.1
local res, err = self.http_client.post(self.introspection_url , { token = token, token_type_hint = 'access_token'}, opts)
if res and err then
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the if res really necessary? Shouldn't this be just if err ?


if res.status == 200 then
local token_info = cjson.decode(res.body)
return token_info
Copy link
Contributor

Choose a reason for hiding this comment

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

This can return nil when cjson.decode returns nil, err. That can happen for example when the response body is not valid json (html usually).

Copy link
Contributor

Choose a reason for hiding this comment

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

Just when fixing #626 I realised there is plenty of error cases.

If the API would return null then this would be ngx.null and return the same error as in #626.

This function should always return a table even when the json response can be a string, number, null, ...

And it should normalize active because if ngx.null is truthy so { "active": null } would evaluate to true.

Copy link
Contributor

@mikz mikz left a comment

Choose a reason for hiding this comment

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

There is one potential crash that should be fixed:
https://github.com/3scale/apicast/pull/619/files#r170911776

The rest is just nitpicks and should be easy to fix.
And we will need a changelog entry.

"type": "string"
},
"clientId": {
"description": "Clilent ID for the Token Introspection Endpoint",
Copy link
Contributor

Choose a reason for hiding this comment

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

A typo here. Should be Client ID.

"description": "Introspection Endpoint URL",
"type": "string"
},
"clientId": {
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like this name is not the same as in the tests/code. Everywhere it uses client_id.

"description": "Clilent ID for the Token Introspection Endpoint",
"type": "string"
},
"clientSecret": {
Copy link
Contributor

Choose a reason for hiding this comment

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

@mikz mikz changed the title Add token introspection feature as a plicy. Add token introspection feature as a policy. Feb 27, 2018
@tmogi001
Copy link
Contributor Author

tmogi001 commented Mar 1, 2018

I fixed my source code and added test cases (unit, integration) of invalid response from IdP.
And also added a change log entry.

"configuration": {
"type": "object",
"properties": {
"introspectionUrl": {
Copy link
Contributor

Choose a reason for hiding this comment

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

This does not match the use in the policy. Here it is introspectionUrl but there it is introspection_url.
We plan to have JSON schema validation during Policy initialization to verify those cases in CI, but right now it is just manual process, sorry.

local access_token = authorization.token
--- Introspection Response must have an "active" boolean value.
-- https://tools.ietf.org/html/rfc7662#section-2.2
if not introspect_token(self, access_token).active then
Copy link
Contributor

Choose a reason for hiding this comment

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

This will incorrectly validate case when the response is { "active": null }. It should check for active == true.

ngx.log(ngx.ERR, 'failed to parse token introspection response:', decode_err)
return { active = false }
end
return token_info
Copy link
Contributor

Choose a reason for hiding this comment

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

This will return whatever the JSON was. So if the JSON is null this would crash a bit later.

I think it should verify type(token_info) == 'table' and return the error otherwise instead of checking just the decode_err.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In my opinion, checking the decode_err is enough. Because it must be an error when token_info is null.
So, cjson.decode() always return error and decode_err must not be null when token_info JSON is null.

I checked cjson.decode(""),cjson.decode("{}"), cjson.decode(nil) and cjson.decode(ngx.null) returned decode_err.

Copy link
Contributor

Choose a reason for hiding this comment

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

If the response body is null then the JSON will be correctly decoded to ngx.null like:

cjson.decode('null')

That will later crash on trying to access .active (attempt to index a userdata value):

cjson.decode('null').active

The same can happen when a number is returned like:

cjson.decode('42').active

What I'm saying is introspect_token always has to return a table, because later it tries to get the key .access from it. And the only way to verify it always returns table is to check the type.
Because the response body is basically user input it can be any valid JSON, including just string "null" or numbers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for information. I missed 'null' and numbers.

Do you think it is good as following? This worked fine with test code.

if decode_err or not (type(token_info) == 'table') then
  ngx.log(ngx.ERR, 'failed to parse token introspection response:', decode_err)
  return { active = false }
end

Copy link
Contributor

@mikz mikz Mar 1, 2018

Choose a reason for hiding this comment

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

I think it could be easier to read as:

if type(token_info) == 'table' then
  return token_info
else
  ngx.log(ngx.ERR, 'failed to parse token introspection response: ', decode_err or 'not a table')
  return { active = false }
end

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you! I think this is nice.

@mikz
Copy link
Contributor

mikz commented Mar 1, 2018

Thank you @tmogi001 for this excellent PR! 🎉

You did a really good job.

@mikz mikz merged commit dbbf2fb into 3scale:master Mar 1, 2018
@mikz mikz changed the title Add token introspection feature as a policy. Add token introspection feature as a policy. [THREESCALE-312] Apr 24, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants