0

This is my decode method written in python:

import base64

from flask import request, jsonify
import jwt
from jwt.exceptions import InvalidTokenError
from config import SECRET_KEY


def authenticate_token(func, require_premium=False):
    """

    :param require_premium: True if the endpoint is only for premium users
    :param func: the function that is being wrapped around
    :return: returns the wrapped function itself
    """

    def wrapper(*args, **kwargs):
        token = request.headers.get('Authorization')
        print(token)
        if not token:
            return jsonify(message='Authorization token is missing'), 401

        # encoded_secret = base64.urlsafe_b64encode(SECRET_KEY.encode('utf-8'))
        try:
            encoded_secret = base64.urlsafe_b64encode(SECRET_KEY.encode('utf-8')).decode('utf-8')
            decoded = jwt.decode(token, encoded_secret, algorithms=['HS512'])
            # Perform additional validation or retrieve user information from the decoded token if needed
            # ...
            if require_premium:
                pass
                # Add a premium user check here
        except InvalidTokenError as e:
            return jsonify(message=f'Invalid token: {e}'), 401

        return func(*args, **kwargs)

    return wrapper

The secret key:

SECRET_KEY = 'exampleSecret'

However, this doesn't work when decoding this token: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsIm5nbyI6Ik5HTyIsInZlcmlmaWVkIjp0cnVlLCJleHAiOjE2ODQ1MTc5NTQsImlhdCI6MTY4NDQzMTU1NCwiYXV0aG9yaXRpZXMiOlsiTk9STUFMX1VTRVIiLCJBRE1JTiJdfQ.xzXMd7GmK_ZEqY2DaSkMiv0axgfMXHZavXTnmNBTn-q96b39coGy2MZL7txNbtDZ1DaUIDzjN6_mVMbtZH1ONg.

I tried it out in the debugger available in jwt.io. You can find it here.

I have also set the secret key correctly before decoding it. I tried it both with and without this line: encoded_secret = base64.urlsafe_b64encode(SECRET_KEY.encode('utf-8')).decode('utf-8') but didn't work on both the cases. What exactly could be the issue here?

The reason I do the base64 encoding is because in the jwt.io debugger, the signature was not verified without it. I had to check "secret base64 encoded" for it to work.

When I print out the error, I get Invalid Header Padding.

Edit (added java code for generating token):

@Component
public class JwtTokenGenerator {
    /**
     * Time in milliseconds the JWT token is valid for.
     */
    public static final long JWT_TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
    /**
     * Time provider to make testing easier.
     */
    private final transient TimeProvider timeProvider;
    @Value("${jwt.secret}")  // automatically loads jwt.secret from resources/application.properties
    private transient String jwtSecret;

    @Autowired
    public JwtTokenGenerator(TimeProvider timeProvider) {
        this.timeProvider = timeProvider;
    }

    /**
     * Generate a JWT token for the provided user.
     *
     * @param userDetails The user details
     * @return the JWT token
     */
    public String generateToken(CustomUserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("ngo", (userDetails).getNgo());
        claims.put("verified", (userDetails).isVerified());
        List<String> authorities = userDetails.getAuthorities()
            .stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
        claims.put("authorities", authorities);
        return Jwts.builder().setClaims(claims).setSubject(userDetails.getUsername())
            .setIssuedAt(new Date(timeProvider.getCurrentTime().toEpochMilli()))
            .setExpiration(new Date(timeProvider.getCurrentTime().toEpochMilli() + JWT_TOKEN_VALIDITY))
            .signWith(SignatureAlgorithm.HS512, jwtSecret).compact();
    }
}

I notice that the .signWith() method expects a base64 encoded key. So it does decode it before signing.

Edit 2 (problem with decoding the encoded key):

import base64

from flask import request, jsonify
import jwt
from jwt.exceptions import InvalidTokenError
from config import SECRET_KEY


def authenticate_token(func, require_premium=False):
    """

    :param require_premium: True if the endpoint is only for premium users
    :param func: the function that is being wrapped around
    :return: returns the wrapped function itself
    """

    def wrapper(*args, **kwargs):
        token = request.headers.get('Authorization').split()[1]
        print(token)
        if not token:
            return jsonify(message='Authorization token is missing'), 401

        try:
            decoded_key = base64.b64decode(SECRET_KEY).decode('utf-8')
            print(decoded_key)
            decoded = jwt.decode(token, decoded_key, algorithms=['HS512'])
            # Perform additional validation or retrieve user information from the decoded token if needed
            # ...
            if require_premium:
                pass
                # Add a premium user check here
        except InvalidTokenError as e:
            return jsonify(message=f'Invalid token: {e}'), 401

        return func(*args, **kwargs)

    return wrapper

This is how my code looks right now. However, I have a problem base64 decoding the key. My encoded key is: pZStXzAznM4xjxnRNgM2Nr5GQYtktG664iFxsVmvj3mPWqki9WrO1y/eGPmo/8T83BDR9l7hRJihRgwe7UGV5A==

After the base64.b64decode(SECRET_KEY), I get this: b'\xa5\x94\xad_03\x9c\xce1\x8f\x19\xd16\x0366\xbeFA\x8bd\xb4n\xba\xe2!q\xb1Y\xaf\x8fy\x8fZ\xa9"\xf5j\xce\xd7/\xde\x18\xf9\xa8\xff\xc4\xfc\xdc\x10\xd1\xf6^\xe1D\x98\xa1F\x0c\x1e\xedA\x95\xe4' which clearly is a byte string. So to I used decode('utf-8') and now I get this error:

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xa5 in position 0: invalid start byte

Kaushik
  • 187
  • 3
  • 13
  • Your question is a little bit confusing. What is the exact error message and where do you get it. *this doesn't work when encoding this token: * - I think you mean **decoding**. What does the error have to do with the secret, and why do you base64Url encode it? Could you please share the secret? Would make sorting out the problem easier. – jps May 31 '23 at 19:17
  • @jps Oh yeah, I did mean `decoding`. And I edited my question to add all the details to make my question clearer. I don't think the error has anything to do with the secret because the secret key is the same (exampleSecret) when the jwt token is generated. – Kaushik May 31 '23 at 19:54
  • Ok, it's clearer now. Where does the token come from? I see some major misunderstandings regarding the secret and the base64 encoding and the general use of the jwt.io debugger. At first, you probably could not validate the token on jwt.io (maybe you really used the wrong secret. Then you clicked on "Secret base64 encoded" and all of a sudden got a validated signature. That was mistake/misunderstanding #1. In the moment you clicked on secret base64 encoded, jwt.io recalculated the signature and automatically validated it, but that isn't the original signature anymore. continued below... – jps May 31 '23 at 20:18
  • See my answer [here](https://stackoverflow.com/questions/69862105/jwt-io-says-signature-verified-even-when-key-is-not-provided/69862239#69862239) which explains the correct steps to validate a signature and avoid false validation. You basically told jwt.io to treat the secret as a base64 string force it to decode the secret first. See also [here](https://stackoverflow.com/questions/52387275/what-does-secret-base64-encoded-on-jwt-io-mean-and-how-would-i-simulate-it-wit/52388404#52388404) – jps May 31 '23 at 20:19
  • Now the second mistake is the logic of "secret base64 encoded". It seems you then used the token with the changed signature from jwt.io in your code and try to validate it. The signature was created based on the secret 'exampleSecret' with "secret base64 encoded". Again, that means for jwt.io that the secret **is** base64 encoded and needs to be decoded. Under these preconditions you would need to also **base64 decode** the secret, not encode it. But as all this is based on a misunderstanding of the usage of jwt.io, you actually don't have to encode or decode the secret at all. – jps May 31 '23 at 20:25
  • I hope this clarifies the problems with the signature validation. However, I'm still not sure where the `Invalid Header Padding.` error comes from. Was it the result of your exception handling in `InvalidTokenError as e:`? I don't think that a wrong secret would cause such an error. And the token itself (the 3 parts of the token) uses Base64Url encoding, which has no padding. – jps May 31 '23 at 20:33
  • Maybe it has to do with the fact that the token text also included the term "Bearer". So it was `Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsIm5nbyI6Ik5HTyIsInZlcmlmaWVkIjp0cnVlLCJleHAiOjE2ODQ1MTc5NTQsImlhdCI6MTY4NDQzMTU1NCwiYXV0aG9yaXRpZXMiOlsiTk9STUFMX1VTRVIiLCJBRE1JTiJdfQ.xzXMd7GmK_ZEqY2DaSkMiv0axgfMXHZavXTnmNBTn-q96b39coGy2MZL7txNbtDZ1DaUIDzjN6_mVMbtZH1ONg `. However, if I only include the token, I get a new error that makes more sense: `Signature verification failed`. – Kaushik May 31 '23 at 20:41
  • And for the jwt.io debugger, I tried using the order you mentioned in one of the answers (first, set the key, then paste the token). When I do that, the signature is verified for the key `exampleSecret` and when the `secret base64 encoded` is checked. I can also share the code for generating code but it was done in springboot java. – Kaushik May 31 '23 at 20:41
  • *included the term "Bearer"* - oh yes, that is definitely a mistake (a quite common one). You need to pass the pure token without "Bearer". *Signature verification failed* makes sense. – jps May 31 '23 at 20:44
  • Oh btw I meant "I can also share the code for generating _the token_ but it was done in springboot java" – Kaushik May 31 '23 at 20:48
  • If you tried it with the token provided in the question, then yes, "exampleSecret" and "secret base64 encoded" is correct for that one, but I believe it's either not the original token, or you had also code in place to Base64 decode the secret before you signed in your code. – jps May 31 '23 at 20:48
  • yes, feel free to share the code. – jps May 31 '23 at 20:50
  • Added it in the latest edit! – Kaushik May 31 '23 at 20:53
  • 1
    Ok, [found something](https://stackoverflow.com/a/71594142/7329832). So the `SignWith` function of jjwt seems to expect a Base64 encoded secret. That explains why you had to click on "secret base64 encoded" on jwt.io. But then you should create a real Base64 encoded secret (of at least 64 Bytes for HS512). So take a 64 Byte random String (or better random byte sequence) and base64 encode it. In your python code you have to decode it before using, because `jwt.decode` will no decode it. – jps May 31 '23 at 21:12
  • So I'd have to store the base64 encoded string in the config file and then decode it in the function before passing it on to jwt.decode(...)? – Kaushik May 31 '23 at 22:21
  • Now I have another problem. I will explain it in the edit 2 – Kaushik May 31 '23 at 22:32
  • Alright, I managed to fix it by encoding the token itself into a byte string instead of trying to decode the byte string key. – Kaushik Jun 01 '23 at 07:42
  • What do you mean with *encoding the token itself into a byte string*? I think the real problem is here: *base64.b64decode(SECRET_KEY).decode('utf-8')* - you don't need the `.decode('utf-8')` part. Just decode the Base64 and pass the result to `jwt.decode`. If the Base64 contains binary data (something that was originally not a readable string), decode to UTF-8 will fail, like in your case. – jps Jun 01 '23 at 07:49
  • What I meant was, I passed `token.encode('utf-8')` this into `jwt.decode` instead of the token? And as you mentioned, I kept the secret key as `base64.b64decode(SECRET_KEY)`. And this seems to have fixed the issue – Kaushik Jun 01 '23 at 07:54

0 Answers0