Verify JWT signature with RSA public key in PHP - 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!

Related

Decoding JWE with web-token/jwt-framework in PHP takes too much time

I need to solve how to decode a JWE encrypted with our public key in PHP.
The case is that the userinfo of Oauth2 is encrypted and needs to be decrypted for the login to go through.
With the current code the userifo is decrypted but it takes up to 13 minutes which is not acceptable for logging in.
This is done in Totara 15 (based on Moodle) and with a hook and a local plugin so as not to have too much core changes.
The code I have now is using "web-token/jwt-framework": "2.2.11" as T15 does not support PHP 8.1 which is required for the latest version.
As far as I can tell web-token/jwt-framework should be able to do this efficiently. However if anyone has other suggestions then I am ready to test other frameworks as well.
As I have not worked with web-token/jwt-framework any suggestions are helpful.
So here is what I have at the moment. It's a function that takes the encoded JWE, a keyid and the path to our private key and returns the decoded payload.
I have debugged the code and it's only the line:
$jwe = $jweLoader->loadAndDecryptWithKey($token, $jwk, $recipient);
That takes time, everything else takes fractions of seconds.
<?php
namespace local_oauth2_extension;
require_once(__DIR__ . '/../vendor/autoload.php');
use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Encryption\Algorithm\KeyEncryption\A256KW;
use Jose\Component\Encryption\Algorithm\KeyEncryption\RSAOAEP;
use Jose\Component\Encryption\Algorithm\ContentEncryption\A256CBCHS512;
use Jose\Component\Encryption\Algorithm\ContentEncryption\A128CBCHS256;
use Jose\Component\Encryption\Compression\CompressionMethodManager;
use Jose\Component\Encryption\Compression\Deflate;
use Jose\Component\Encryption\JWEDecrypter;
use Jose\Component\Core\JWK;
use Jose\Component\Encryption\Serializer\JWESerializerManager;
use Jose\Component\Encryption\Serializer\CompactSerializer;
use Jose\Component\Encryption\JWELoader;
use Jose\Component\Checker\HeaderCheckerManager;
use Jose\Component\Checker\AlgorithmChecker;
use Jose\Component\Signature\JWSTokenSupport;
class jwe_helper
{
public static function decode_response($token, $kid, $pem_path)
{
try{
$jwk = JWKFactory::createFromKeyFile(
$pem_path, // The filename
null,
[
'use' => 'enc', // Additional parameters
'kid' => $kid
]
);
// The key encryption algorithm manager with the A256KW algorithm.
$keyEncryptionAlgorithmManager = new AlgorithmManager([
new RSAOAEP()
]);
// The content encryption algorithm manager with the A256CBC-HS256 algorithm.
$contentEncryptionAlgorithmManager = new AlgorithmManager([
new A128CBCHS256()
]);
// The compression method manager with the DEF (Deflate) method.
$compressionMethodManager = new CompressionMethodManager([
new Deflate(),
]);
// We instantiate our JWE Decrypter.
$jweDecrypter = new JWEDecrypter(
$keyEncryptionAlgorithmManager,
$contentEncryptionAlgorithmManager,
$compressionMethodManager
);
// The serializer manager. We only use the JWE Compact Serialization Mode.
$serializerManager = new JWESerializerManager([
new CompactSerializer(),
]);
$jweLoader = new JWELoader(
$serializerManager,
$jweDecrypter,
null
);
$jwe = $jweLoader->loadAndDecryptWithKey($token, $jwk, $recipient);
$payload = $jwe->getPayload();
$payload = base64_decode($payload);
$userinfo = self::extract_userinfo($payload);
return $userinfo;
} catch (\Exception $exception) {
return $token;
}
return $token;
}
........
}
As per the documentation
For RSA-based encryption algorithms, it is highly recommended to install GMP or BCMath extension.
It is also mentioned after composer install
GMP or BCMath is highly recommended to improve the library performance
You should make sure either GMP or BCMath is installed on your platform.

Change tymon-jwt algo in header

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?

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.

How to sign SOAP message in php

I'm using php (yii2) and I'd like to implement SOAP communication with server. I have following guide to SOAP:
The Customer’s system uses the Customer’s private key for issuing
digital signatures. Both the application request (ApplicationRequest)
and the SOAP message must be signed separately in the WSC. The
signature is performed with the private key. The signing system must
include in the signature also the certificate. This certificate
contains the public key corresponding to the private key used in the
signing. The receiver uses the public key to authenticate the
signature.
and:
Next step: Digitally sign (detached type XML Digital Signature) the
whole SOAP message with the Private Key of Sender Certificate and put
the signature into SOAP-header
So, I have own private.key, public.key and certificate.cer
My code looks like
$client = new SoapClient($wdsl, ['trace' => true]);
$arguments = ['DownloadFileListRequest' => $dflr];
$appResponse = $client->__call('downloadFileList', $arguments);
But I get the expected error:
SOAP signature error
What I have to do and how to sign this SOAP?
XMLSecurityDSig helped (https://github.com/robrichards/xmlseclibs)
$dom = new DOMDocument('1.0', 'UTF-8');
$ar = $dom->createElementNS('http://bxd.fi/xmldata/', 'ApplicationRequest');
$dom->appendChild($ar);
$ar->appendChild($dom->createElement('CustomerId', $this->userID));
...
$ar->appendChild($dom->createElement('Content', $contentBase64));
$objDSig = new XMLSecurityDSig();
$objDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);
$objDSig->addReference(
$dom,
XMLSecurityDSig::SHA256,
['http://www.w3.org/2000/09/xmldsig#enveloped-signature'],
['force_uri' => true]
);
$objKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, ['type'=>'private']);
$objKey->loadKey($this->privateKeyPath, true);
$objDSig->sign($objKey);
$objDSig->add509Cert(base64_encode(file_get_contents($this->certificatePath)), false);
$objDSig->appendSignature($dom->documentElement);
$xmlRaw = $dom->saveXML();

Categories