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

Suggest passing block to JWT.decode for claim verification #110

Open
kjwierenga opened this issue Oct 9, 2015 · 7 comments
Open

Suggest passing block to JWT.decode for claim verification #110

kjwierenga opened this issue Oct 9, 2015 · 7 comments

Comments

@kjwierenga
Copy link
Contributor

The current method for verifying the JTI field in the token payload is only useful when the JTI value to compare against is known a priori (when making the call to JWT.decode).

Summary: Using a block parameter or callback on JWT.decode to verify claims (passing the decoded payload/claims) would be more flexible and nicer API IMHO

When the JTI value is dependent on information in the token payload (for example a user object found using a user_id field in the payload), then we can't pass the expected JTI value in the options hash to JWT.decode. In this case we don't know the expected value until after the token has been decoded. The same goes for comparing a JTI value to a blacklist.

The usefulness of the options {verify_jti: true, jti: 'expected-value'} passed to JWT.decode is limited. Maybe the same goes for iss, aud and sub.

May I suggest an alternative API for verification of jti, iss, aud and sub which is to pass an (anonymous) block to JWT.decode for verification. The payload would be passed as parameters to the block. When the block returns falsey, then JWT.decode would raise a JWT::VerificationError.

For example: Given the following verification function ...

def valid_token?(payload)
  valid_user?(payload['user']['id']) && !blacklisted?(payload['jti'])
end

... with the current API I have to do:

payload, header = JWT.decode(token, 'my-jwt-secret')
valid_token?(payload) || raise AuthenticationError # application specific exception

... With a verification block this would become:

JWT.decode(token, 'my-jwt-secret') do |payload|
  valid_token?(payload)
end # when valid_token? return falsey raises JWT::VerificationError (for example)

The current verfication of the exp, nbf and iat claims is unambiguous so it can be handled in JWT.decode with the current flag-and-expected-value-in-options-hash method. But I would prefer the block method to be the only method to verfiy claims. Special 'build-in' methods could be used for verification of exp, nbf and iat claims.

JWT.decode(token, 'my-jwt-secret') do |payload, header|
  valid_exp?(payload['exp']) && \ # built-in
  valid_nbf?(payload['nbf']) && \ # built-in
  valid_iat?(payload['iat']) && \ # built-in
  valid_token?(payload) # user defined
end # when valid_token? return falsey raises JWT::ValidationError (for example)

This method doesn't require a separate flags to indicate which claims to verify.

Sorry this has gotten a bit long. I hope I explained well.

@kjwierenga kjwierenga changed the title Suggest passing block to JWT.decode for token validation Suggest passing block to JWT.decode for token verification Oct 10, 2015
@kjwierenga kjwierenga changed the title Suggest passing block to JWT.decode for token verification Suggest passing block to JWT.decode for claim verification Oct 10, 2015
@kjwierenga
Copy link
Contributor Author

I've also looked at how JWT libraries in other languages such as Perl support claim verification. The Perl library supports passing a regular expression or callback function, but the callback function only receives the specific claim value as argument which doesn't solve our original problem.

The json_web_token Ruby gem simply returns a hash of all claims and lets the application figure things out from there. Clean and simple.

An alternative API closer to the current options style would be to have a separate callback closure for each claim and pass the (rest of the) payload (or claims as I prefer to name them) as an optional second argument to a proc. This would replace the flag-and-expected-value-in-options-hash by a single proc for claim verification.

Example:

JWT.decode(token, 'my-jwt-secret', true, {
  verify_jti: proc { |jti,claims| valid_token?(claims) }, # with extra claims argument
  verify_aud: proc { |aud| 'app' == aud } # without claims argument
})

Now it seems strange to pass both a specific claim and all claims (optionally) to the callback proc. Why not have a single verify callback?

JWT.decode(token, 'my-jwt-secret', true, {
  verify: lambda { |claims|
    valid_token?(claims) && 'app' == claims['aud']
  } # can use lambda; no optional arguments
})

@ak47
Copy link

ak47 commented Oct 27, 2015

+1 for this,
just working through JWT validation and this is exactly the question I had - if the JTI is sent out, and I have no other record of it, how do I supply it to the verify unless it is pulled from the payload?

awkward

@excpt
Copy link
Member

excpt commented Feb 10, 2016

@ak47 Have a look at #126 Just merged a PR that should improve JTI verification a little.

@mikz
Copy link

mikz commented Feb 22, 2016

Block passed to JWT.decode is already used as a keyfinder.

That has a significant issue, it is just passed the header, not the payload. So it can't find key based on the payload.

I'd propose passing procs/lambdas/objects that respond to call instead of passing blocks.
Also, using keyword arguments instead of positional ones, for better user experience and minimize fault.
Also, use objects instead of tuples of data. So instead of header, payload, signature, signing_input use JWT::Token which would hav attributes header, payload, signature, signing_input.

My use case is multi-tenant key verification and decoding like https://auth0.com/blog/2015/03/10/blacklist-json-web-token-api-keys/ describes in detail.

JWT.decode('some-token',
  key: ->(token) { KeyStore.find(token.payload['aud'] },
  verify: ->(token) { KeyStore.verify_revocation(token) }
) 

To sum it up:

  • JWT::Token for representing the token instead of array of 4 elements
  • keyword arguments (Ruby >= 2.0)
  • accept objects that respond to #call or #===
  • injecting custom lambdas everywhere allows more customization like multi-tenancy and revocation

@excpt
Copy link
Member

excpt commented Feb 22, 2016

@mikz +1 Great proposal with a fitting use case.

@kjwierenga
Copy link
Contributor Author

@mikz sounds good to me +1

@wenderjean
Copy link

wenderjean commented Feb 20, 2019

If there is no one working on that, I'd like to write some code based on discussion and @mikz elaboration. cc/ @excpt

@anakinj anakinj removed this from the Version 3.0 milestone Jul 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants