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

Wrong type of exception raised if required claim is missing in jwt.decode #197

Open
akikoskinen opened this issue Nov 24, 2020 · 2 comments

Comments

@akikoskinen
Copy link

The documentation of jwt.decode says:

Raises:
JWTError: If the signature is invalid in any way.
ExpiredSignatureError: If the signature has expired.
JWTClaimsError: If any claim is invalid in any way.

When an explicitly required claim (one where options["require_XXX"] is set to True) is missing, a JWTError is raised. I think the implementation and the documentation contradict. I like what the documentation says, and wish that the implementation would deliver. So in this case I hope it would raise a JWTClaimsError.

@blag
Copy link
Contributor

blag commented Nov 24, 2020

You raise an interesting question: if a required claim is not provided, should that claim be considered "invalid"?

I can definitely see your point that yes, it should be considered "invalid", but after reviewing the code I'm not so sure that I agree with you:

def _validate_claims(claims, audience=None, issuer=None, subject=None,
                     algorithm=None, access_token=None, options=None):
    ...

    # Check that all required claims are provided
    for require_claim in [
        e[len("require_"):] for e in options.keys() if e.startswith("require_") and options[e]
    ]:
        if require_claim not in claims:
            raise JWTError('missing required key "%s" among claims' % require_claim)
        else:
            options['verify_' + require_claim] = True  # override verify when required

    ...

    # Each of these blocks validates a claim value if the claim should be verified
    # The `_validate_<claim>` functions all raise JWTClaimsError
    if options.get('verify_iat'):
        _validate_iat(claims)

    if options.get('verify_nbf'):
        _validate_nbf(claims, leeway=leeway)

    ...

I think the original intent in the code was to classify errors for misconfiguration, decoding, or missing (required) claims as JWT errors, and to classify errors about the values of claims themselves as invalid value errors.

To wit, the slightly more generic JWTError is raised in the following cases:

In comparison, the JWTClaimsError is raised in the following cases:

So the code differentiates between these two JWT claims (assume that the "issuer" claim is required):

{
  "aud": "..."
  // required issuer claim is just completely missing from the claims
}  // raises a JWTError

versus:

{
  "aud": "..."
  "iss": null  // required issuer claim is provided, but decodes to None, which isn't a valid value
}  // raises a JWTClaimsError

Circling back around to the question you raised, I think that a completely missing claim should be handled differently than a claim value that is not valid.

Now, that's not to say that your question is invalid to begin with. At the very least, the documentation and docstrings should make this difference very clear:

    Raises:
        JWTError: If the signature is invalid in any way, including
                  decoding issues, missing claims, a non-JSON
                  header or claims section, or invalid signatures.
        ...
        JWTClaimsError: If any provided claim value is invalid in any way.

Would that rewording make the difference more clear?

An additional idea would be to create a JWTMissingClaimError subclass of JWTError:

class JWTMissingClaimError(JWTError):
    "Raised when a required JWT claim is not provided."
    pass

and raise that in _validate_claims if a claim is missing. That will be reverse compatible with code that should already be checking for JWTError exceptions for missing claims. I haven't thought through the potential security implications of giving possible attackers more information though, so that's a change that should be well thought through and communicated to users (eg: major version bump, which will probably happen anyway when I drop Python 2.7 support after this next release that I'm trying to work on, so that would be a good time to make this change).

I would be open to pull requests implementing either or both of those if you have the time. And I'll keep this issue open until it's fixed.

@akikoskinen
Copy link
Author

Whoa, that's something I call a thorough answer 🙇

My original problem was that I couldn't tell apart "invalid signature" and "missing required claim" cases (just using the exception type). That would be solved by the proposed JWTMissingClaimError type.

Another thing that I've now noticed is that we might interpret the term signature differently. To me signature means only the cryptographic signature, the one that jws.verify checks. But the proposed documentation:

If the signature is invalid in any way, including decoding issues, missing claims, a non-JSON header or claims section, or invalid signatures.

makes me think that here the term signature (its first occurrence) includes more, perhaps the JWT as a whole. If this is the case, perhaps I interpreted the original documentation ("JWTError: If the signature is invalid in any way") also differently than how it was meant.

So, in order to avoid confusion, we should either agree on what signature means, or acknowledge, that we have differing interpretations. Off course this isn't just you or me making interpretations of the term, since this is public documentation. If there's a chance of misinterpretation, then the documentation could off course be improved.

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

2 participants