Change tymon-jwt algo in header - php

I'm trying to create a custom token using Tymon-JWT with a different algorithm than the one defined in the config file (jwt.php).
public function generateCustomToken($data) {
$claims = [...] // standard claims in a JWT token
// set the algorithm
JWTAuth::getJWTProvider()->setAlgo('RS256');
// custom signer as well
JWTAuth::getJWTProvider()->setSecret($data["signer"]);
// if I log it here the correct algorithm is displayed
Log::info(JWTAuth::getJWTProvider()->getAlgo()); == 'RS256'
$claims = new Collection($requiredClaims);
$payload = new Payload($claims, new PayloadValidator());
$tkn = JWTAuth::encode($payload);
return $tkn->get();
}
So I change the algorithm and log it and it shows my value but when I decode the token the header "alg" displays the algorithm set in the config file.
Does this mean that it is not using RS256 to encode the token? How do I go about changing that algo value?

Related

Cognito token being used before issued at time

i'm using the Cognito authorization code to get my access tokens from AWS Cognito. So this is my current workflow:
No session data, forward user to hosted UI.
On login, return to PHP Application with the authorization code.
Use authorization code to get the tokens.
Validate the tokens using the jwk tokens.
So step 4 is where i'm stuck, as when validating the tokens, JWT is starting that i'm trying to decode the access_token before it is valid:
Cannot handle token prior to 2022-05-26T13:45:17+0000
I have dumped out the code which displays this error to get the iat time and the time that it is being compared against:
dd($payload->iat, ($timestamp + static::$leeway));
This outputs:
1653576011 1653576009
So there is a 0000000002 difference...
Now I can fix this by changing leeway to 10, but this doesn't seem the correct thing to do... any ideas?
Below is the code:
private function validateTokens($accessToken): bool
{
$domain = implode('.', [ 'cognito-idp', $this->region, 'amazonaws.com' ]);
$jwks = file_get_contents("https://$domain/{$this->pool_id}/.well-known/jwks.json");
$jwks = collect(json_decode($jwks, true, 512, JSON_THROW_ON_ERROR)['keys']);
$jwks = $jwks->keyBy('kid')->map(static function ($current) {
return (new JWKConverter())->toPEM($current);
})->toArray();
$jwt = new JWT();
$jwt::$leeway = 10;
$decoded = $jwt::decode($accessToken['access_token'], $jwks, ['RS256']);
dd($decoded);

productId is ignored and returned as null when validating Android In App Product via purchases.products.get

I'm trying to validate a Google Play/Android In App Product consumed purchase server-side using PHP. I get a response back with a valid receipt, but there are two confusing issues:
The productId is always null
If I change the the $productId in the sample below to an invalid ID, it will return the exact same response. This seems to be the case for literally any string.
Here is my sample code:
$purchaseToken = 'TEST purchaseToken FROM ANDROID APP';
$appId = 'com.example.myapp';
$productId = 'com.example.myapp.iap1';
$googleClient = new \Google_Client();
$googleClient->setScopes([\Google_Service_AndroidPublisher::ANDROIDPUBLISHER]);
$googleClient->setApplicationName($appId);
$googleClient->setAuthConfig(__DIR__ . '/gp-service.json');
$googleAndroidPublisher = new \Google_Service_AndroidPublisher($googleClient);
$purchase = $googleAndroidPublisher->purchases_products->get($appId, $productId, $purchaseToken);
If I dump out $purchase, I get:
=> Google_Service_AndroidPublisher_ProductPurchase {
+acknowledgementState: 1,
+consumptionState: 1,
+developerPayload: "",
+kind: "androidpublisher#productPurchase",
+obfuscatedExternalAccountId: null,
+obfuscatedExternalProfileId: null,
+orderId: "GPA.XXXX-XXXX-XXXX-XXXXX",
+productId: null,
+purchaseState: 0,
+purchaseTimeMillis: "1602771022178",
+purchaseToken: null,
+purchaseType: 0,
+quantity: null,
+regionCode: "US",
}
Does anyone know what I am doing wrong here? It doesn't seem to be validating the productId on its end nor does it provide me the data I would need to validate it on my end, meaning I have no way of validating this IAP right now.
https://issuetracker.google.com/issues/169870112
I've found on google issue tracker that ignoring productId is an intended behaviour. I've wrote post asking if the null that we receive in response is also an intended behaviour. I hope not because as you wrote if productId will be always null we have no way of fully validating IAP right now.
$purchaseToken was undefined in Josh's answer.
<?php
$appId = "com.example.myapp";
$serviceAccountJson = __DIR__ . "/gp-service.json";
// this is the raw receipt data received on the device from Google Play; this example is obfuscated and only shows the keys for sensitive fields
$googlePlayReceipt =
'{"productId": "com.example.myapp.iap1","transactionDate": 1602714720893,"transactionReceipt": "","purchaseToken": "","transactionId": "","dataAndroid": "","signatureAndroid": "","isAcknowledgedAndroid": false,"autoRenewingAndroid": false,"purchaseStateAndroid": 1}';
// decode the json to an array we can use
$decodedGooglePlayReceipt = json_decode($googlePlayReceipt, true);
// the data that was signed for verification purposes
$data = $decodedGooglePlayReceipt["transactionReceipt"];
// the signature that was used to sign the $data
$signature = $decodedGooglePlayReceipt["signatureAndroid"];
// The "Base64-encoded RSA public key" for your app, taken from the Google Play Console
// In the Classic Console: Your App -> Development Tools -> Services & APIs -> Licensing & in-app billing
// In the New Console: Your App -> Monetize -> Monetization Setup -> Licensing
$base64EncodedPublicKeyFromGoogle = "################";
// Convert the key into the normal public key format
// Just need to split the base64 key into 64 character long lines and add the usual prefix/suffix
$openSslFriendlyKey =
"-----BEGIN PUBLIC KEY-----\n" .
chunk_split($base64EncodedPublicKeyFromGoogle, 64, "\n") .
"-----END PUBLIC KEY-----";
// Convert the key to the openssl key ID that openssl_verify() expects
// I'm unsure if this step would be necessary on all platforms
$publicKeyId = openssl_get_publickey($openSslFriendlyKey);
// Use openssl_verify() to verify the $signature (which has to be base64 decoded!) against the $data using the public key we have
$result = openssl_verify(
$data,
base64_decode($signature),
$publicKeyId,
OPENSSL_ALGO_SHA1
);
if ($result === 0) {
throw new Exception("Invalid receipt");
}
if ($result !== 1) {
throw new Exception(openssl_error_string());
}
// receipt data is valid. now let's grab the productId and purchaseToken
$decodedData = json_decode($data, true);
$purchasedProductId = decodedData["productId"];
$purchaseToken = decodedData["purchaseToken"];
// now we'll verify that the receipt is valid for our account
try {
$googleClient = new \Google_Client();
$googleClient->setScopes([
\Google_Service_AndroidPublisher::ANDROIDPUBLISHER,
]);
$googleClient->setApplicationName($appId);
$googleClient->setAuthConfig($serviceAccountJson);
$googleAndroidPublisher = new \Google_Service_AndroidPublisher(
$googleClient
);
$purchase = $googleAndroidPublisher->purchases_products->get(
$appId,
$purchasedProductId,
$purchaseToken
);
} catch (Throwable $exception) {
// this means the receipt data is unable to be validated by Google
throw new Exception("Invalid receipt");
}
After seeing Deusald's response here and starting to type up a response to the Google Issues ticket complaining about it, I had an epiphany: Google is just validating that the transaction is valid for your account and expects you to validate the receipt data server-side. They include a base64 encoded RSA SHA1 signature in the receipt data and the original data they used to create that signature, so they give you everything you need to accomplish this.
The below snippet accomplishes that verification for PHP, but it should be easily portable to other languages:
<?php
// our app's bundle id
$appId = 'com.example.myapp';
// the location of a Service Account JSON file for a Service account that has access to the "Financial Data" permissions in the Play Console
$serviceAccountJson = __DIR__ . '/gp-service.json';
// this is the raw receipt data received on the device from Google Play; this example is obfuscated and only shows the keys for sensitive fields
$googlePlayReceipt = '{"productId": "com.example.myapp.iap1","transactionDate": 1602714720893,"transactionReceipt": "","purchaseToken": "","transactionId": "","dataAndroid": "","signatureAndroid": "","isAcknowledgedAndroid": false,"autoRenewingAndroid": false,"purchaseStateAndroid": 1}';
// decode the json to an array we can use
$decodedGooglePlayReceipt = json_decode($googlePlayReceipt, true);
// the data that was signed for verification purposes
$data = $decodedGooglePlayReceipt['transactionReceipt'];
// the signature that was used to sign the $data
$signature = $decodedGooglePlayReceipt['signatureAndroid'];
// The "Base64-encoded RSA public key" for your app, taken from the Google Play Console
// In the Classic Console: Your App -> Development Tools -> Services & APIs -> Licensing & in-app billing
// In the New Console: Your App -> Monetize -> Monetization Setup -> Licensing
$base64EncodedPublicKeyFromGoogle = '################';
// Convert the key into the normal public key format
// Just need to split the base64 key into 64 character long lines and add the usual prefix/suffix
$openSslFriendlyKey = "-----BEGIN PUBLIC KEY-----\n" . chunk_split($base64EncodedPublicKeyFromGoogle, 64, "\n") . "-----END PUBLIC KEY-----";
// Convert the key to the openssl key ID that openssl_verify() expects
// I'm unsure if this step would be necessary on all platforms
$publicKeyId = openssl_get_publickey($openSslFriendlyKey);
// Use openssl_verify() to verify the $signature (which has to be base64 decoded!) against the $data using the public key we have
$result = openssl_verify($data, base64_decode($signature), $publicKeyId, OPENSSL_ALGO_SHA1);
if ($result === 1) {
// receipt data is valid. now let's grab the productId and purchaseToken
$decodedData = json_decode($data, true);
$purchasedProductId = decodedData['productId'];
$purchaseToken = decodedData['purchaseToken'];
// now we'll verify that the receipt is valid for our account
try {
$googleClient = new \Google_Client();
$googleClient->setScopes([\Google_Service_AndroidPublisher::ANDROIDPUBLISHER]);
$googleClient->setApplicationName($appId);
$googleClient->setAuthConfig($serviceAccountJson);
$googleAndroidPublisher = new \Google_Service_AndroidPublisher($googleClient);
$purchase = $googleAndroidPublisher->purchases_products->get($appId, $purchasedProductId, $purchaseToken);
} catch (Throwable $exception) {
// this means the receipt data is unable to be validated by Google
throw new Exception('Invalid receipt');
}
} elseif ($result === 0) {
throw new Exception('Invalid receipt');
} else {
throw new Exception(openssl_error_string());
}

Keycloak : validating access_token using PHP

Assuming that I received token after managed to login through openid-connect
http://xxxxxx/auth/realms/demo/protocol/openid-connect/token
{
"access_token": "xxxxxx",
"expires_in": 600,
"refresh_expires_in": 1800,
"refresh_token": "xxxxxx",
"token_type": "bearer",
"not-before-policy": xxxx,
"session_state": "xxxxx",
"scope": "email profile"
}
Is there any ways on how to decode the payload of the jwt tokens just like the https://jwt.io/ did , using PHP? Thank you.
You can use this library https://github.com/firebase/php-jwt.
Why do you want to decode the access_token though? Usually it's the id_token that gets decoded so that the client can verify the end-user's identity. The process of decoding requires the JWT to have its signature verified.
You can use the library I mentioned above. The steps are easy. You need:
The JWT
Secret Key/ Public Key
Algorithm used to encode the JWT
The following snippet is used to decode + verify a JWT. It uses HS256 so a secret key must be in the possession of the client:
$decoded = JWT::decode($jwt, $key, array('HS256'));
If you want to decode a JWT without verifying its signature (unsafe), you can create a function that separates each of the JWT section: header, body, and signature, and base64url decode it. Like so:
// Pass in the JWT, and choose which section. header = 0; body = 1; signature = 2
public function decodeJWT($jwt, $section = 0) {
$parts = explode(".", $jwt);
return json_decode(base64url_decode($parts[$section]));
}
EDIT if you're decoding + verifying an id_token which uses assymetric algorithm e.g. RSA256, RSA384 etc, you need the public key. OpenID Connect defines a JWK Set endpoint (/.well-known/jwks.json), which lists the public keys in JWK format. You can hit that endpoint and save the response in an array. In order to find which public key was used, the JWK has a kid claim/ property. Which represents the key id, the identifier of the public key. You can decode your id_token and grab its header using :
$header = decodeJWT($id_token, 0);
Then you can pass the header to the function below to get the key that was used to encode the id_token. Parameter $keys holds the JWK Set response:
function getIdTokenKey($keys, $header) {
foreach ($keys as $key) {
if ($key->kty == 'RSA') {
if (!isset($header->kid) || $key->kid == $header->kid) {
return $key;
}
}
}
throw new Exception("key not found");
}
$key = getIdTokenKey($keys, $header);
Finally call the decode function, assume it's using RSA256:
$decoded = JWT::decode($id_token, $key, array('RSA256'));
Edit(2) On another note it's the same process to decode any JWT, be it an access token, id token, or arbitrary data being passed to different entities in a server environment.

Laravel 5 Creating Custom Tokens And Parsing Back Using JWT

I am a newbie to JWT Token System in laravel 5 and using tymon JWT Auth
I managed to create my custom JWT token and my code as follows
use App\Http\Requests;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Tymon\JWTAuth\JWTManager as JWT;
use JWTAuth;
use JWTFactory;
use Tymon\JWTAuth\Exceptions\JWTException;
public function login(Request $request)
{
$customClaims = ['foo' => 'bar', 'baz' => 'bob'];
$payload = JWTFactory::make($customClaims);
$token = JWTAuth::encode($payload);
// return response()->json(compact('token')); // This didnt work?Why?
return response()->json(compact($token))->header('Authorization','Bearer '.$token);
}
public function getUser(){
$token = JWTAuth::parseToken();
echo $token;
}
Here are my following clarifications required
// return response()->json(compact('token'));
Why this gave me an empty json object as {"token":{}}
Is it the right way, i could send my custom data in token and get it back the foo and baz values from the same token?
The output of my code while testing with postman is an empty array. as []. But my headers are added with Authorization →Bearer eyJ0eXAiOiJKV1QiLCJhbG...
Is this correct?
3a. Instead of a simple blank array, i need a success message as 'authorized':true. How can i achieve it?
How should i pass this token back to test. Where should this token be passed using postman. I passed it through Headers as shown in the image
How could i parse this token using laravel and get the custom data i.e foo and baz sent as a token. The method i called is getUser here.
I dont think the token creation is being built properly. Below is working code for login token creation. For this, make sure that the 'user' model under your config/jwt.php is the correct eloquent user model for your application.
$user = array(
'user' => $request->input('email'),
'password' => $request->input('pass')
);
$customClaims= ['usr' => $user['user']];
if(!$token = JWTAuth::attempt($user, $customClaims)){
abort(401);
}
else{
return response()->json(compact('token'));
}
Also included in the above code with the custom claims variable, you were on the right track with that just needs to be passed as a second parameter in the attempt function.
Only the client needs to send the authorization: Bearertoken header to prove that they are who they say they are (I am coming from an android client/server jwt background. So sorry if this doesnt apply to your application).
3a. For any subsequent pages that the user browses to, you simply add an if statement like this
if(!$user = JWTAuth::parseToken()->authenticate()){
abort(401);
}
else{
// Code allowing the user to see protected content
}
See answer to question 3. include an http header with authorization BearerToken
To extract the data from the JWT Payload, you will need to decode the base64 encoded text from the text after the first period in the token and send that to a string. Then run that string through the base64_decode($string) function. That should start to give you some of the payload data.
Hope this helps.
I had the same problem here and i got the following solution:
public function whatEver()
{
$token = JWTAuth::parseToken();
$response = $token->getPayload()->get('foo');
return $response
}
this should return bar.
you can use this method in your user model :
/**
* Return a key value array, containing any custom claims to be added to the JWT.
*
* #return array
*/
public function getJWTCustomClaims()
{
return [
'perms' => '
'
];
}

Verify JWT signature with RSA public key in PHP

In PHP, I'm trying to validate an AWS auth token (JWT returned from getOpenIdTokenForDeveloperIdentity) using the AWS's RSA public key (which I generated from modulus/exponent at https://cognito-identity.amazonaws.com/.well-known/jwks_uri). The key begins with the appropriate headers/footers -----BEGIN RSA PUBLIC KEY----- etc. I've looked at a few PHP libraries like Emarref\Jwt\Jwt, however I get the error: error:0906D06C:PEM routines:PEM_read_bio:no start line. It all boils down to the basic php function: openssl_verify.
I've looked at the php.net/manual for openssl-verify, but I'm still not clear on the parameter details. The algorithm needed is RS512.
I am able to verify the JWT token using node.js with no problems (same key and token). For that I used the library: https://github.com/auth0/node-jsonwebtoken
Not sure why this doesn't work in PHP. Can I not use an RSA Public Key?
function verifyKey($public_key) {
$jwt = new Emarref\Jwt\Jwt();
$algorithm = new Emarref\Jwt\Algorithm\Rs512();
$factory = new Emarref\Jwt\Encryption\Factory();
$encryption = $factory->create($algorithm);
$encryption->setPublicKey($public_key);
$context = new Emarref\Jwt\Verification\Context($encryption);
$token = $jwt->deserialize($authToken);
try {
$jwt->verify($token, $context);
} catch (Emarref\Jwt\Exception\VerificationException $e) {
debug($e->getMessage());
}
}
Could you try using another PHP library: https://github.com/Spomky-Labs/jose
// File test.php
require_once __DIR__.'/vendor/autoload.php';
use Jose\Checker\ExpirationChecker;
use Jose\Checker\IssuedAtChecker;
use Jose\Checker\NotBeforeChecker;
use Jose\Factory\KeyFactory;
use Jose\Factory\LoaderFactory;
use Jose\Factory\VerifierFactory;
use Jose\Object\JWKSet;
use Jose\Object\JWSInterface;
// We create a JWT loader.
$loader = LoaderFactory::createLoader();
// We load the input
$jwt = $loader->load($input);
if (!$jws instanceof JWSInterface) {
die('Not a JWS');
}
// Please note that at this moment the signature and the claims are not verified
// To verify a JWS, we need a JWKSet that contains public keys (from RSA key in your case).
// We create our key object (JWK) using a RSA public key
$jwk = KeyFactory::createFromPEM('-----BEGIN RSA PUBLIC KEY-----...');
// Then we set this key in a keyset (JWKSet object)
// Be careful, the JWKSet object is immutable. When you add a key, you get a new JWKSet object.
$jwkset = new JWKSet();
$jwkset = $jwkset->addKey($jwk);
// We create our verifier object with a list of authorized signature algorithms (only 'RS512' in this example)
// We add some checkers. These checkers will verify claims or headers.
$verifier = VerifierFactory::createVerifier(
['RS512'],
[
new IssuedAtChecker(),
new NotBeforeChecker(),
new ExpirationChecker(),
]
);
$is_valid = $verifier->verify($jws, $jwkset);
// The variable $is_valid contains a boolean that indicates the signature is valid or not.
// If a claim is not verified (e.g. the JWT expired), an exception is thrown.
//Now you can use the $jws object to retreive all claims or header key/value pairs
I was able to get this library to work. However I had to build the key using KeyFactory::createFromValues instead of KeyFactory::createFromPEM. THANK YOU!

Categories