-
-
Notifications
You must be signed in to change notification settings - Fork 367
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
Conversation
} | ||
|
||
// validate the token | ||
if err = t.Claims.Valid(); err != nil { | ||
if err != nil { | ||
if e, ok := err.(*jwtx.ValidationError); ok { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
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. |
Signed-off-by: Christopher Brown <ccbrown112@gmail.com>
1e512fe
to
6ada567
Compare
There was a problem hiding this 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, |
There was a problem hiding this comment.
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{} { |
There was a problem hiding this comment.
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 | ||
} |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 <ccbrown112@gmail.com>
794638a
to
8e4543b
Compare
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. |
Yup, the |
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? |
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.) |
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 |
Sounds fair to me. 👍 I'll see if something good comes to mind, and either update the PR with something or remove the |
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()) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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{}) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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 |
I think I have those things... The compose factory is |
Sorry, I probably simply overlooked it! Will review it again later today or latest tomorrow |
Also, can you verify that your implementation works as expected with the example: https://github.com/ory-am/fosite-example ? :) |
Yep. Works fine as far as I can tell. |
Sorry, I completely forgot about this issue. I will get to it next week! |
Thank you for your contribution! |
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.