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

handler/oauth2: allow stateless introspection of jwt access tokens #141

Merged
merged 4 commits into from
Feb 15, 2017

Conversation

ccbrown
Copy link
Contributor

@ccbrown ccbrown commented Jan 21, 2017

Conversation started in #140. A quick draft was requested by @arekkas, which I provided there, but I figured starting a review might be a better way to talk about the code than inline code blocks and patch files. I won't be offended if this isn't consistent with the roadmap. The functionality is either going into my application code or fosite, so it's time well spent either way.

The goal here is to be able to perform validation of JWT access tokens without hitting the storage backend.

Without too much effort, we can define an introspector to do it. No public symbols or behavior are changed. Just added a few things that make this nicer.

}

// validate the token
if err = t.Claims.Valid(); err != nil {
if err != nil {
if e, ok := err.(*jwtx.ValidationError); ok {
Copy link
Contributor Author

@ccbrown ccbrown Jan 21, 2017

Choose a reason for hiding this comment

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

This entire block is dead code btw. I didn't change it because I was going for minimal impact and didn't want to alter the behavior, but the RS256JWTStrategy.Decode method already does all of the validation, and it always returns a new string error. 😞

screen shot 2017-01-21 at 4 45 57 am

Copy link
Member

Choose a reason for hiding this comment

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

Ah right, this should be errors.Cause(err) instead

}
}

func BenchmarkIntrospectJWT(b *testing.B) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There aren't any other benchmarks, so maybe performance is a non-goal of the library, but this was of interest to me. Understood if you want to delete in the interest of staying slim. It's certainly not useful if no one ever runs it.

Copy link
Member

Choose a reason for hiding this comment

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

👍

@aeneasr
Copy link
Member

aeneasr commented Jan 21, 2017

Thanks for the PR!

I strongly dislike that scopes are currently not supported - it's a core OAuth2 concept and not using it is an anti-pattern, so it shouldn't be encouraged by the code base!

Apart from that, I understand the case now better. One issue I have is that token recovation does not have an effect when the JWT strategy is used. This could be very misleading and mount up to a security issue when the library is configured incorrectly by a developer.

Don't be mistaken though, I would like to support statelessness, I think it's pretty cool.

Also, I think it would be essential to have some sort of spec test for the validation strategies - so something that tests the different implementations against the same test data. Maybe you could refactor existing integration tests to make that work.

Copy link
Contributor Author

@ccbrown ccbrown left a comment

Choose a reason for hiding this comment

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

In order to support validating scopes, we have to write them to the JWTs. There's not a registered IANA key for that, but there is one drafted here, which I've seen a few other libraries using: https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-07

@@ -75,22 +75,22 @@ func TestDoesClientWhiteListRedirect(t *testing.T) {
isError: true,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Some random go fmt changes in this file.

//
// Due to the stateless nature of this factory, THE BUILT-IN REVOCATION MECHANISMS WILL NOT WORK.
// If you need revocation, you can validate JWTs statefully, using the other factories.
func OAuth2StatelessJWTIntrospectionFactory(config *Config, storage interface{}, strategy interface{}) interface{} {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Definitely agreed that there's an increased risk of misconfiguration. Hoping renaming this to emphasize the statelessness and adding a cautionary doc block will minimize that risk.


if claims.Issuer == "" {
claims.Issuer = h.Issuer
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There seemed to be a lot of cases where these things would be left empty, making it difficult to use JWTs interchangeably in tests.

Noticed the OpenID strategy had this Issuer field, which I added here just because it's handy.

ret["nbf"] = float64(c.NotBefore.Unix()) // jwt-go does not support int64 as datatype

if !c.IssuedAt.IsZero() {
ret["iat"] = float64(c.IssuedAt.Unix()) // jwt-go does not support int64 as datatype
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Filtering out values of -62135596800 from JWTs for the optional fields.

Signed-off-by: Christopher Brown <[email protected]>
@ccbrown
Copy link
Contributor Author

ccbrown commented Jan 22, 2017

I had to rewrite the history for those last two commits since I just realized my email address in the "signed-off-by" line was wrong. Hope that doesn't make looking at this too weird.

@aeneasr
Copy link
Member

aeneasr commented Jan 22, 2017

Yup, the scp claim is what I'd be doing as well!

@aeneasr
Copy link
Member

aeneasr commented Jan 22, 2017

How about a recovation strategy that comes with this introspection strategy and that always returns an error on revocation? Then combine them in a composer wrapper. Could that be a solution?

@ccbrown
Copy link
Contributor Author

ccbrown commented Jan 22, 2017

I did consider making it a runtime error by doing that or something similar, but I think there may be valid use-cases for custom revocation handlers with this introspector. Not really sure if that's a common enough need to consider it though. (Personally, I don't have that need.)

@aeneasr
Copy link
Member

aeneasr commented Jan 22, 2017

Ok, if we don't find a solution I'd probably accept the refactoring required for your implementation to work, but would not include the implementation itself. You could provide a package fosite-stateless-validation and we'd link it in the docs. Then, in the readme, you could point out that token revocation won't work with this code base. What do you think about this? I think it's a fair trade off between your specific use case, a benefit for the open source eco system, and a way to keep the concepts within fosite unisono and repeatable

@ccbrown
Copy link
Contributor Author

ccbrown commented Jan 22, 2017

Sounds fair to me. 👍

I'll see if something good comes to mind, and either update the PR with something or remove the StatelessJWTValidator struct.

@ccbrown
Copy link
Contributor Author

ccbrown commented Jan 23, 2017

Went with the return-an-error-on-revocation idea. I think the valid use-cases for revocation with this type of JWT validation would require writing entirely new custom handlers anyways.

switch e.Errors {
case jwtx.ValidationErrorMalformed:
return errors.Wrap(fosite.ErrInvalidTokenFormat, err.Error())
err = errors.Wrap(fosite.ErrInvalidTokenFormat, err.Error())
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure right now, does golang fallthrough or break per default?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It definitely feels weird coming from other languages that just fall through, but Go breaks if you don't add a fallthrough statement: https://golang.org/ref/spec#Switch_statements

claims.ExpiresAt = jwtSession.GetExpiresAt(tokenType)

if claims.ExpiresAt.IsZero() {
claims.ExpiresAt = jwtSession.GetExpiresAt(tokenType)
Copy link
Member

Choose a reason for hiding this comment

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

The idea here was to explicitly override the expires_at value. What's the reasoning to move it into IsZero?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm. I don't remember if this was intentional or if it was meant to be temporary while I was testing... Doesn't have any impact on tests, so I'd better change it back.

return func(rw http.ResponseWriter, req *http.Request) {
req.ParseForm()
ctx := fosite.NewContext()

accessRequest, err := oauth2.NewAccessRequest(ctx, req, &fosite.DefaultSession{})
accessRequest, err := provider.NewAccessRequest(ctx, req, &oauth2.JWTSession{})
Copy link
Member

Choose a reason for hiding this comment

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

Why is this no longer the default Session?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It has to be JWTSession for TestIntrospectToken to be able to test any of the JWT strategies.

@aeneasr
Copy link
Member

aeneasr commented Jan 25, 2017

Thanks, that looks pretty solid, I love that we have scopes in JWTs now!

I've added a few comments. We would also need some clarification in the docs that the JWT Stateless Strategy isn't supporting Token Revocation, and also a compose factory that sets up the stateless strategy appropriately

@ccbrown
Copy link
Contributor Author

ccbrown commented Jan 25, 2017

I think I have those things... The compose factory is compose.OAuth2StatelessJWTIntrospectionFactory, and that and StatelessJWTValidator both document the lack of revocation. Did you have something else in mind?

@aeneasr
Copy link
Member

aeneasr commented Jan 25, 2017

Sorry, I probably simply overlooked it! Will review it again later today or latest tomorrow

@aeneasr
Copy link
Member

aeneasr commented Jan 25, 2017

Also, can you verify that your implementation works as expected with the example: https://github.com/ory-am/fosite-example ? :)

@ccbrown
Copy link
Contributor Author

ccbrown commented Jan 25, 2017

Yep. Works fine as far as I can tell.

@aeneasr
Copy link
Member

aeneasr commented Feb 11, 2017

Sorry, I completely forgot about this issue. I will get to it next week!

@aeneasr aeneasr self-assigned this Feb 11, 2017
@aeneasr
Copy link
Member

aeneasr commented Feb 15, 2017

Thank you for your contribution!

@aeneasr aeneasr merged commit 14a18a7 into ory:master Feb 15, 2017
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.

2 participants