Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

How to verify a jwt ? #214

Closed
mridah opened this issue Jun 27, 2018 · 14 comments
Closed

How to verify a jwt ? #214

mridah opened this issue Jun 27, 2018 · 14 comments

Comments

@mridah
Copy link

mridah commented Jun 27, 2018

Suppose I get a jwt string xxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxx

Now, how will I verify this jwt ???

How will I convert this string back to an array to decode / verify it ? @bshaffer @mbleigh @ultrasaurus

@cottton
Copy link

cottton commented Jun 28, 2018

JWT::decodedecode($jwt, $key, array $allowed_algs)

see https://github.com/firebase/php-jwt/blob/master/src/JWT.php#L69
Also there are examples here: https://github.com/firebase/php-jwt

@mridah
Copy link
Author

mridah commented Aug 30, 2018

Thanks @cottton, that solved the problem :)

Also I would like to know one more thing: How to I revoke a jwt token ?

@cottton
Copy link

cottton commented Aug 30, 2018

Actually you cannot.

  • (auth-) server generates a JWT and handles it out to the client
  • client calls another service and provides the JWT
  • service validates the JWT and can trust the client

The JWT gives the client access as long

  1. the JWT the "exp" claim (expiration Time) is valid

  2. the public key is valid

  3. an id in the JWT is not blacklisted

  4. "exp" claim defines i.e. 5 min. So the client has a ticket for 5 min. After the 5 min the client needs to get a new access token (using f.e. a refresh token).

  5. The service(s) gets updated and the public key changed. The JWT will not be valid anymore.

  6. "Shared blacklist". F.e. the (auth-) server provides|distributes a black list. A service gets i.e. the user_id from the JWT and finds it on the shared blacklist. Access gets denied.
    Instead of blacklisting the user you could also blacklist another id that identifies the JWT.

@aamakerlsa
Copy link

I'm trying to verify tokens in a test environment (now that I am able to successfully create tokens)... but I get this error after I manually change the secret / key for the decode attempt:

Fatal error: Uncaught Error: Class 'Firebase\JWT\SignatureInvalidException' not found in /My/Path/here/php-jwt-master/php-jwt-master-src/JWT.php:112 Stack trace: #0 /My/Path/here/php-jwt-master/testjwt2.php(44): Firebase\JWT\JWT::decode('eyJ0eXAiOiJKV1Q...', '7772', Array) #1 {main} thrown in /My/Path/here//php-jwt-master/php-jwt-master-src/JWT.php on line 112

what do I need to do to NOT get the fatal error?

@cottton
Copy link

cottton commented Nov 5, 2018

Class 'Firebase\JWT\SignatureInvalidException' not found
Autoload?
Or for test manually use require_once.

@aamakerlsa
Copy link

@cottton - solved it... I needed to add require for each of the exception classes:

require_once 'php-jwt-master-src/BeforeValidException.php';
require_once 'php-jwt-master-src/ExpiredException.php';
require_once 'php-jwt-master-src/SignatureInvalidException.php';
... in addition to the JWT.php require I already had

and in my try catch I had to use:

try { $usertoken_to_verify = $_GET["usertoken"]; $decodejwt = JWT::decode($usertoken_to_verify, $key, array('HS256')); print_r($decodejwt); } catch (\Exception $e) { echo $e; }

@vedmant
Copy link

vedmant commented Dec 10, 2019

What if I need to decode token first and then only verify it agains a key, how can I do this? I'll need to get "kid" value from the token before checking it signature, but JWT::decode() already requires key to use.

@bshaffer
Copy link
Collaborator

To decode a string without verifying it against the public key, you can do this:

use Firebase\JWT\JWT;
$tks = explode('.', $jwt);
if (count($tks) != 3) {
    throw new UnexpectedValueException('Wrong number of segments');
}
list($headb64, $bodyb64, $cryptob64) = $tks;
$payload = JWT::jsonDecode(JWT::urlsafeB64Decode($bodyb64));

This is not supported directly in the library because of the possibility someone could use this unintentionally without verifying the signature.

@cottton
Copy link

cottton commented Dec 10, 2019

What if I need to decode token first and then only verify it agains a key, how can I do this? I'll need to get "kid" value from the token before checking it signature, but JWT::decode() already requires key to use.

Had the same problem.
The kid *1

  • is in the first segment of the JWT
  • is json encoded
  • is part of the signature (included to created the signature)
    .
    You actually should check if the signature is valid before using data from the JWT.
    BUT you cannot, if the system is using the kid from the header.
    Is SHOULD be save to extract the kid from the header
    and then check if the signature is valid.

I wrote a method to get the header.
You then can get the kid from the array and decode the JTW:

/**
 * Returns the access token header.
 *
 * @param string $accessTokenString
 *
 * @return array|null
 */
public function getHeader($accessTokenString)
{
    #code copied from JWT::decode
    $tks = explode('.', $accessTokenString);
    if (count($tks) != 3) {
        throw new \UnexpectedValueException('Wrong number of segments');
    }
    list($headb64, $bodyb64, $cryptob64) = $tks;
    if (null === (
        $header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64))
        )
    ) {
        throw new \UnexpectedValueException('Invalid header encoding');
    }
    return json_decode(json_encode($header), true);
}

*1 (For those who dont know) The kid (key id) is an id that points to one of n public keys.
When the auth server creates a JWT then it sets the kid on the JTW header. The server could f.e. use PHP filemtime on the public key file (if it is a file) to create a kid.
A client then can check via kid which public key it should use.

@cottton
Copy link

cottton commented Dec 10, 2019

To decode a string without verifying it against the public key, you can do this:

use Firebase\JWT\JWT;
$tks = explode('.', $jwt);
if (count($tks) != 3) {
    throw new UnexpectedValueException('Wrong number of segments');
}
list($headb64, $bodyb64, $cryptob64) = $tks;
$payload = JWT::jsonDecode(JWT::urlsafeB64Decode($bodyb64));

This is not supported directly in the library because of the possibility someone could use this unintentionally without verifying the signature.

Do NOT use the body (content) without verifying the JWT!

@vedmant
Copy link

vedmant commented Dec 11, 2019

Thanks, I ended up duplicating code from JWT::decode partially to just decode header. I think it should have some separate method to just decode JWT. For example npm package "jsonwebtoken" has separate method to decode and verify and that's perfectly ok.

@psignoret
Copy link

psignoret commented Dec 11, 2019

@vedmant Instead of decoding the header yourself, you can pass in a map of kid => key values to JWT::decode(). The library will take care of selecting the key with the kid value matching the one identified in the header.

$possible_keys = [
    'kid1' => 'my_key1', 
    'kid2' => 'my_key2',
];
$decoded = JWT::decode( $msg, $possible_keys, ['RS256'] );

It would be very dangerous to offer a method to decode without validating. It would be too easy for a distracted developer to accidentally use this instead of the method which also validates, opening up a significant vulnerability in their application.

@vedmant
Copy link

vedmant commented Dec 11, 2019

@psignoret Thanks, I actually ended up doing exactly this. I also had to decode jwks into keys, here is the code if someone needs it:

use phpseclib\Crypt\RSA;
use phpseclib\Math\BigInteger;

/**
     * Convert jwks to key
     *
     * @param array $data
     * @return RSA
     */
    protected function jwksToKey(array $data): RSA
    {
        switch ($data['kty']) {
            case 'RSA':
                $rsa = new RSA();
                $n = new BigInteger('0x' . bin2hex(JWT::urlsafeB64Decode($data['n'])), 16);
                $e = new BigInteger('0x' . bin2hex(JWT::urlsafeB64Decode($data['e'])), 16);
                if (array_key_exists('d', $data)) {
                    throw new UnexpectedValueException('RSA private key isn\'t supported');
                } else {
                    $pem_string = $rsa->_convertPublicKey($n, $e);
                }
                $rsa->loadKey($pem_string);

                return $rsa;
            default:
                throw new UnexpectedValueException('Unknown key type');
        }
    }

@bshaffer
Copy link
Collaborator

bshaffer commented Dec 13, 2019

@cottton You do not need to decode the header to use the kid, the library supports decoding using the kid when an array of public keys are passed in as the second argument to decode with the kids as array keys:

$keys = [
   'abcdefg' => $publicKey1,
   '1234567' => $publicKey2,
];

// throws an exception if the `kid` is empty or doesn't exist, otherwise it uses the
// mapped array value to verify the signature, e.g. "$publicKey1"
$payload = Firebase\JWT\JWT::decode($jwt, $keys, ['RS256']);

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants