How to decode WooCommerce Webhook Secret? - php

I can't find any information on what algorithm to use to decode WooCommerce webhook field X-Wc-Webhook-Signature in PHP. Does anyone know how to decode it?
Thanks!

Expanding on the current answers, this is the PHP code snippet you need:
$sig = base64_encode(hash_hmac('sha256', $request_body, $secret, true));
Where $secret is your secret, $request_body is the request body, which can be fetched with file_get_contents('php://input');
The $sig value should then be equal to the X-Wc-Webhook-Signature request header.

To expand on the laravel solution this is how I created middleware to validate the incoming webhook.
Create middleware. The application that I am using keeps the WooCommerce consumer key and secret in a table assigned to a given store.
class ValidateWebhook
{
/**
* Validate that the incoming request has been signed by the correct consumer key for the supplied store id
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle(Request $request, Closure $next)
{
$signature = $request->header('X-WC-Webhook-Signature');
if (empty($signature)) {
return response(['Invalid key'], 401);
}
$store_id = $request['store'];
$consumer_key = ConsumerKeys::fetchConsumerSecretByStoreId($store_id);
$payload = $request->getContent();
$calculated_hmac = base64_encode(hash_hmac('sha256', $payload, $consumer_key, true));
if ($signature != $calculated_hmac) {
return response(['Invalid key'], 401);
}
return $next($request);
}
}
Register the middleware in Kernel.php
'webhook' => \App\Http\Middleware\ValidateWebhook::class,
Protect the webhook route with the middleware
Route::post('webhook', 'PrintController#webhook')->middleware('webhook');

Here is my solution for this question. You need to generate a sha256 hash for the content that was sent [payload] and encode it in base64. Then just compare the generated hash with the received one.
$secret = 'your-secret-here';
$payload = file_get_contents('php://input');
$receivedHeaders = apache_request_headers();
$receivedHash = $receivedHeaders['X-WC-Webhook-Signature'];
$generatedHash = base64_encode(hash_hmac('sha256', $payload, $secret, true));
if($receivedHash === $generatedHash):
//Verified, continue your code
else:
//exit
endif;

According to the docs:
"A base64 encoded HMAC-SHA256 hash of the payload"
I'm guessing that the payload in this instances is the secret you've supplied in the webhook properties.
Source: https://woocommerce.github.io/woocommerce-rest-api-docs/v3.html#webhooks-properties
EDIT
Further digging indicates that the payload is the body of the request.
So you'd use a HMAC library to create a sha256 hash of the payload using your webhook secret, and then base64 encode the result, and do the comparison with X-Wc-Webhook-Signature and see if they match.
You can generate a key pair from the following URL:
/wc-auth/v1/authorize?app_name=my_app&scope=read_write&user_id=<a_unique_id_that_survives_the_roundtrip>&return_url=<where_to_go_when_the_flow_completes>&callback_url=<your_server_url_that_will_receieve_the_following_params>
These are the params the callback URL receieves in the request body:
{
consumer_key: string,
consumer_secret: string,
key_permissions: string,
user_id: string,
}

Here's my laravel / lumen function to verify the webhook request, hope it helps someone!
private function verifyWebhook(Request $request) {
$signature = $request->header('x-wc-webhook-signature');
$payload = $request->getContent();
$calculated_hmac = base64_encode(hash_hmac('sha256', $payload, 'WOOCOMMERCE_KEY', true));
if($signature != $calculated_hmac) {
abort(403);
}
return true;
}

Related

Laravel is returning a XSRF-COOKIE decrypted as cookie

So I'm following the default setup as in the Sanctum documentation, i make a request o csrf cookie route so Laravel triggers the Cookies and set to the request the XSRF-TOKEN, but the documentation says:
Laravel stores the current CSRF token in an encrypted XSRF-TOKEN cookie that is included with each response generated by the framework. You can use the cookie value to set the X-XSRF-TOKEN request header.
https://laravel.com/docs/9.x/csrf#csrf-x-csrf-token
This is the code of frontend i retrieve the Cookie every time and send in the X-XSRF-TOKEN header
const authLink = setContext(async (_, { headers }) => {
const authToken = localStorage.getItem(AUTH_TOKEN_NAME)
console.log(Cookie.get('XSRF-TOKEN'))
return {
headers: {
...headers,
authorization: authToken ? `Bearer ${authToken}` : '',
'X-XSRF-TOKEN': Cookie.get('XSRF-TOKEN')
}
}
})
Of course, this is executed everytime a request is made, so here is the relevant console log:
As you can see, for some reason Laravel is sending randomly a decrypted cookie in plain text, and some times a encrypted one.
Of course, it's important that only the encrypted cookie should be returned because the header X-XSRF-TOKEN is decrypted on backend every time, as you can see in \Illuminate\Foundation\Http\Middleware\VerifyCsrfToken:
/**
* Get the CSRF token from the request.
*
* #param \Illuminate\Http\Request $request
* #return string
*/
protected function getTokenFromRequest($request)
{
$token = $request->input('_token') ?: $request->header('X-CSRF-TOKEN');
if (! $token && $header = $request->header('X-XSRF-TOKEN')) {
try {
$token = CookieValuePrefix::remove($this->encrypter->decrypt($header, static::serialized()));
} catch (DecryptException $e) {
$token = '';
}
}
return $token;
}
So, does anyone has any idea why Laravel is returning to me a randomly encrypted and decrypted csrf token, instead of only the encrypted one? Or even weirder, it's returning both tokens, in each request one type?

Testing DocuSign webhooks with a connect key

I am using DocuSign Connect to retrieve webhooks from DocuSign and digest them within my Larave; application. Here is the basic idea.
<?php
namespace App\Http\Controllers;
use App\Http\Middleware\VerifyDocusignWebhookSignature;
use App\Mail\PaymentRequired;
use App\Models\PaymentAttempt;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;
class DocusignWebhookController extends Controller
{
/**
* Create a new controller instance.
* If a DocuSign Connect key is preset, validate the request.
*/
public function __construct()
{
$this->gocardlessTabs = ['GoCardless Agreement Number', 'GoCardless Amount', 'GoCardless Centre'];
$this->assumedCustomer = 2;
if (config('docusign.connect_key')) {
$this->middleware(VerifyDocusignWebhookSignature::class);
}
}
/**
* Handle an incoming DocuSign webhook.
*/
public function handleWebhook(Request $request)
{
$payload = json_decode($request->getContent(), true);
$shouldProcessWebhook = $this->determineIfEnvelopeRelevant($payload);
if ($shouldProcessWebhook) {
switch ($payload['status']) {
case 'sent':
return $this->handleSentEnvelopeStatus($payload);
break;
case 'completed':
return $this->handleCompletedEnvelopeStatus($payload);
break;
case 'voided':
// ...
break;
default:
}
}
}
}
The logic itself works fine but if you look here:
if (config('docusign.connect_key')) {
$this->middleware(VerifyDocusignWebhookSignature::class);
}
If I specify a connect key I run some middleware to verify the webhook came from DocuSign.
The class to verify the signature came from DocuSign and looks like this:
<?php
namespace App\DocuSign;
/**
* This class is used to validate HMAC keys sent from DocuSign webhooks.
* For more information see: https://developers.docusign.com/platform/webhooks/connect/hmac/
*
* Class taken from: https://developers.docusign.com/platform/webhooks/connect/validate/
*
* Sample headers
* [X-Authorization-Digest, HMACSHA256]
* [X-DocuSign-AccountId, caefc2a3-xxxx-xxxx-xxxx-073c9681515f]
* [X-DocuSign-Signature-1, DfV+OtRSnsuy.....NLXUyTfY=]
* [X-DocuSign-Signature-2, CL9zR6MI/yUa.....O09tpBhk=]
*/
class HmacVerifier
{
/**
* Compute a hmac hash from the given payload.
*
* Useful reference: https://www.php.net/manual/en/function.hash-hmac.php
* NOTE: Currently DocuSign only supports SHA256.
*
* #param string $secret
* #param string $payload
*/
public static function computeHash($secret, $payload)
{
$hexHash = hash_hmac('sha256', $payload, utf8_encode($secret));
$base64Hash = base64_encode(hex2bin($hexHash));
return $base64Hash;
}
/**
* Validate that a given hash is valid.
*
* #param string $secret: the secret known only by our application
* #param string $payload: the payload received from the webhook
* #param string $verify: the string we want to verify in the request header
*/
public static function validateHash($secret, $payload, $verify)
{
return hash_equals($verify, self::computeHash($secret, $payload));
}
}
Now, in order to test this locally I've written a test but whenever I run it, the middleware tells me the webhook isn't valid.
Here is my test class
<?php
namespace Tests\Feature\Http\Middleware;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
class VerifyDocusignWebhookSignatureTest extends TestCase
{
use RefreshDatabase, WithFaker;
public function setUp(): void
{
parent::setUp();
config(['docusign.connect_key' => 'probably-best-not-put-on-stack-overflow']);
$this->docusignConnectKey = config('docusign.connect_key');
}
/**
* Given a JSON payload, can we parse it and do what we need to do?
*
* #test
*/
public function it_can_retrieve_a_webhook_with_a_connect_key()
{
Mail::fake();
$payload = '{"status":"sent","documentsUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/documents","recipientsUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/recipients","attachmentsUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/attachments","envelopeUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514","emailSubject":"Please DocuSign: newflex doc test.docx","envelopeId":"2ba67e2f-0db6-46af-865a-e217c9a1c514","signingLocation":"online","customFieldsUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/custom_fields","notificationUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/notification","enableWetSign":"true","allowMarkup":"false","allowReassign":"true","createdDateTime":"2022-02-14T11:36:01.18Z","lastModifiedDateTime":"2022-02-14T11:37:48.633Z","initialSentDateTime":"2022-02-14T11:37:49.477Z","sentDateTime":"2022-02-14T11:37:49.477Z","statusChangedDateTime":"2022-02-14T11:37:49.477Z","documentsCombinedUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/documents/combined","certificateUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/documents/certificate","templatesUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/templates","expireEnabled":"true","expireDateTime":"2022-06-14T11:37:49.477Z","expireAfter":"120","sender":{"userName":"Newable eSignature","userId":"f947420b-6897-4f29-80b3-4deeaf73a3c5","accountId":"366e9845-963a-41dd-9061-04f61c921f28","email":"e-signature#newable.co.uk"},"recipients":{"signers":[{"tabs":{"textTabs":[{"validationPattern":"","validationMessage":"","shared":"false","requireInitialOnSharedChange":"false","requireAll":"false","value":"","required":"true","locked":"false","concealValueOnDocument":"false","disableAutoSize":"false","maxLength":"4000","tabLabel":"GoCardless Amount","font":"lucidaconsole","fontColor":"black","fontSize":"size9","localePolicy":{},"documentId":"1","recipientId":"56041698","pageNumber":"1","xPosition":"319","yPosition":"84","width":"84","height":"22","tabId":"207f970c-4d3c-4d0c-be6b-1f3aeecf5f95","tabType":"text"},{"validationPattern":"","validationMessage":"","shared":"false","requireInitialOnSharedChange":"false","requireAll":"false","value":"","required":"true","locked":"false","concealValueOnDocument":"false","disableAutoSize":"false","maxLength":"4000","tabLabel":"GoCardless Centre","font":"lucidaconsole","fontColor":"black","fontSize":"size9","localePolicy":{},"documentId":"1","recipientId":"56041698","pageNumber":"1","xPosition":"324","yPosition":"144","width":"84","height":"22","tabId":"f6919e94-d4b7-4ef4-982d-3fc6c16024ab","tabType":"text"},{"validationPattern":"","validationMessage":"","shared":"false","requireInitialOnSharedChange":"false","requireAll":"false","value":"","required":"true","locked":"false","concealValueOnDocument":"false","disableAutoSize":"false","maxLength":"4000","tabLabel":"GoCardless Agreement Number","font":"lucidaconsole","fontColor":"black","fontSize":"size9","localePolicy":{},"documentId":"1","recipientId":"56041698","pageNumber":"1","xPosition":"332","yPosition":"200","width":"84","height":"22","tabId":"9495a53c-1f5e-42a5-beec-9abcf77b4387","tabType":"text"}]},"creationReason":"sender","isBulkRecipient":"false","requireUploadSignature":"false","name":"Jesse","firstName":"","lastName":"","email":"Jesse.Orange#newable.co.uk","recipientId":"56041698","recipientIdGuid":"246ce44f-0c11-4632-ac24-97f31911594e","requireIdLookup":"false","userId":"b23ada8e-577e-4517-b0fa-e6d8fd440f21","routingOrder":"1","note":"","status":"sent","completedCount":"0","deliveryMethod":"email","totalTabCount":"3","recipientType":"signer"},{"tabs":{"signHereTabs":[{"stampType":"signature","name":"SignHere","tabLabel":"Signature 7ac0c7c8-f838-4674-9e37-10a0df2f81c1","scaleValue":"1","optional":"false","documentId":"1","recipientId":"38774161","pageNumber":"1","xPosition":"161","yPosition":"275","tabId":"371bc702-1a91-4b71-8c77-a2e7abe3210e","tabType":"signhere"}]},"creationReason":"sender","isBulkRecipient":"false","requireUploadSignature":"false","name":"Jesse Orange","firstName":"","lastName":"","email":"jesseorange360#gmail.com","recipientId":"38774161","recipientIdGuid":"844f781c-1516-4a5a-821a-9d8fb2319369","requireIdLookup":"false","userId":"f544f7ff-91bb-4175-894e-b42ce736f273","routingOrder":"2","note":"","status":"created","completedCount":"0","deliveryMethod":"email","totalTabCount":"1","recipientType":"signer"}],"agents":[],"editors":[],"intermediaries":[],"carbonCopies":[],"certifiedDeliveries":[],"inPersonSigners":[],"seals":[],"witnesses":[],"notaries":[],"recipientCount":"2","currentRoutingOrder":"1"},"purgeState":"unpurged","envelopeIdStamping":"true","is21CFRPart11":"false","signerCanSignOnMobile":"true","autoNavigation":"true","isSignatureProviderEnvelope":"false","hasFormDataChanged":"false","allowComments":"true","hasComments":"false","allowViewHistory":"true","envelopeMetadata":{"allowAdvancedCorrect":"true","enableSignWithNotary":"false","allowCorrect":"true"},"anySigner":null,"envelopeLocation":"current_site","isDynamicEnvelope":"false"}';
// Compute a hash as in production this will come from DocuSign
$hash = $this->computeHash($this->docusignConnectKey, $payload);
// Validate the hash as we're going to use it as the header
$this->assertTrue($this->validateHash($this->docusignConnectKey, $payload, $hash));
// Convert this response to an array for the test
$payload = json_decode($payload, true);
// Post as JSON as Laravel only accepts POSTing arrays
$this->postJson(route('webhook-docusign'), $payload, [
'x-docusign-signature-3' => $hash
])->assertStatus(200);
$this->assertDatabaseHas('payment_attempts', [
'envelope_id' => $payload['envelopeId']
]);
Mail::assertNothingSent();
}
/**
* As we're testing we need a way to verify the signature so we're computing the hash.
*/
private function computeHash($secret, $payload)
{
$hexHash = hash_hmac('sha256', $payload, utf8_encode($secret));
$base64Hash = base64_encode(hex2bin($hexHash));
return $base64Hash;
}
/**
* Validate that a given hash is valid.
*
* #param string $secret: the secret known only by our application
* #param string $payload: the payload received from the webhook
* #param string $verify: the string we want to verify in the request header
*/
private function validateHash($secret, $payload, $verify)
{
return hash_equals($verify, self::computeHash($secret, $payload));
}
}
I'm also using webhook.site to compare hashes:
Given this I can tell you that x-docusign-signature-3 matches the hash I generate when I run
$hash = $this->computeHash($this->docusignConnectKey, $payload);
So, my issue surely must stem from the way I'm sending the data back through?
When you compute your own HMAC on the incoming payload (to see if it matches the HMAC that was sent in the header), you must use the incoming payload as is.
In your code:
public function handleWebhook(Request $request)
{
$payload = json_decode($request->getContent(), true);
$shouldProcessWebhook = $this->determineIfEnvelopeRelevant($payload);
you are sending the json decoded payload to your check method. That is not right, you should send the raw payload, as it arrived.
(Decoding, then encoding JSON doesn't necessarily give you the same byte sequence as the original.)
The JSON decode method should only be applied to the payload after you've confirmed that the payload came from DocuSign.
Plus, doing the JSON decode before you've authenticated the sender is a security issue. A bad guy could be trying to send you some bad input. The rule is trust nothing until you've verified the sender (via the HMAC in this case).
Bonus comment
I recommend that you also configure DocuSign Connect webhook's Basic Authentication feature. Basic Authentication is often checked at the web server level. HMAC, since it must be computed, is usually check at the app level. Using both provides solid defense against bad guys.

How to validate a Huawei X-HW-SIGNATURE?

How can I validate a X-HW-SIGNATURE in PHP?
The documentation for request parameters reads:
Message header signature, which is mandatory, indicating the
signature information sent to your server that receives uplink messages.
There's also example data:
timestamp=1563105451261; nonce=:; value=E4YeOsnMtHZ6592U8B9S37238E+Hwtjfrmpf8AQXF+c=
The keys are:
timestamp: standard Unix timestamp
nonce: colon
value: character string to be encrypted
This here is the part which I don't understand:
timestamp + nonce + Uplink message content: obtained after the encryption using the set password in HMAC-SHA256 algorithm and encoding in Base64.
How can I validate the message payload against the header signature?
What I've tried so far basically is:
private function parse_request_body(): void {
$this->rawBody = stream_get_contents(STDIN);
if (isset($_SERVER['X-HW-SIGNATURE']) && !empty($_SERVER['X-HW-SIGNATURE'])) {
if (! $this->hmac_verify( $this->rawBody, $_SERVER['X-HW-SIGNATURE'] )) {
// spoof message
}
}
}
private function hmac_verify( string $payload, string $signature ): bool {
// the problem obviously lies here ...
return true;
}
This is how i would go about verifying the signature. From my understanding from the doc. However it isn't 100% clear as they do not provide an example, which is a shame...
You should have (or be able to create one) a secret key within your Huawei account somewhere.
private function hmac_verify( string $payload, string $signature ): bool
{
$secretKey = 'yoursecretkey';
$parsedSignature = str_replace(';', '&', $signature); //'timestamp=1563105451261& nonce=:& value=E4YeOsnMtHZ6592U8B9S37238E+Hwtjfrmpf8AQXF+c='
parse_str($parsedSignature, $signatureParts);
// $signatureParts
//
// array(3) {
// ["timestamp"]=>
// string(13) "1563105451261"
// ["nonce"]=>
// string(1) ":"
// ["value"]=>
// string(44) "E4YeOsnMtHZ6592U8B9S37238E Hwtjfrmpf8AQXF c="
// }
$signed = hash_hmac("sha256", $signatureParts['timestamp'] + $signatureParts['nonce'] + $payload, $secretKey);
return base64_encode($signed) === $signatureParts['value'];
}
On another page of the documentation (X-HUAWEI-CALLBACK-ID), I've found a similar description:
Base64-encoded string that has been HMAC-SHA256 encrypted using the callback key. The string before encryption consists of the value of timestamp, value of nonce, and callback user name, without plus signs.
And here it's being described how to send push.hcm.upstream messages on Android. Sending an upstream message might be the best chance to obtain the payload, in order to validate a signature. The server-side procedure upon send as following:
When receiving the uplink message, the Push Kit server will:
Combine the receiving timestamp, colon (:), and uplink message into a character array to be encrypted (for example, 123456789:your_data).
Encrypt the character array in HMAC-SHA256 mode using an HMAC signature verification key, and encode the encrypted result in Base64 to generate a signature.
Transfer the signature and timestamp information to your app server through the X-HW-SIGNATURE and X-HW-TIMESTAMP fields in the HTTPS request header.
Your app server needs to use the HTTPS request header and HMAC signature verification key to verify the validity of the uplink message.
Whatever "an HMAC signature verification key" may be; placeholder your_data sounds alike, as if (likely not yet base64 encoded) $payload->data would have been used to generate the signature:
/** Concatenate the input string, generate HMAC hash with SHA256 algorithm, then encode as base64. */
private function generate_signature( int $timestamp, string $nonce, string $data_str, string $secret_key): string {
$input = $timestamp.$nonce.$data_str;
$hmac = hash_hmac( 'sha256', $input, $secret_key );
return base64_encode( $hmac );
}
/** Convert the received signature string to object. */
private function to_object( string $signature ): stdClass {
$input = str_getcsv( $signature, '; ' );
$data = new stdClass();
$data->timestamp = (int) str_replace('timestamp=', '', $input[0]);
$data->nonce = (string) str_replace( ' nonce=', '', $input[1]);
$data->value = (string) str_replace( ' value=', '', $input[2]);
return $data;
}
public function hmac_verify( string $raw_body, string $secret_key, string $signature ): bool {
/* Extract data-string from the raw body. */
$payload = json_decode( $raw_body );
$data_str = base64_decode( $payload->data );
/* Convert the received signature string to object. */
$signature = $this->to_object( $signature );
/* Generate a signature which to compare to. */
$generated = $this->generate_signature( $signature->timestamp, $signature->nonce, $data_str, $secret_key);
/* Compare the generated with the received signature. */
return $generated === $signature->value;
}
Need to test this once with an actual $_POST ...
The "HMAC signature verification key" (per web-hook) can be obtained from the PushKit console:
Thank you for providing the information regarding this issue. We are very sorry that it brings you inconvinience and are now organizing R&D team to supplement the sample code.
The X-HW-SIGNATURE field is used to check whether the message is from Huawei service.
Usage:
Timestamp + nonce + Uplink message content are combined into a character string. Use the configured HMAC HMAC-SHA256 algorithm and encoding in Base64 to compare the obtained value with the value sent by the push service. If they are the same, the message is from the push service, and you do not need to parse the specific value of this field.

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.

Amazon ElasticSearch service Signature mismatch for PUT Request - Amazon SDK php V2

I am using Amazon ElasticSearch Service and when i tried to create SignatureV4 Request it is working fine for search operations (GET Requests). But when i tried to do some operations like create indices (Using PUT request), it will trough the Signature mismatch error.
I am using Amazon SDK version 2 SignatureV4 library for signing the requests. Also created a custom Elasticsearch handler to add tokens to the request.
Does anybody have such issue with SignatureV4 library in Amazon SDK php V2.
{"message":"The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.\n\nThe Canonical String for this request should have been\n'PUT\n/test_index_2\n\nhost:search-test-gps2gj4zx654muo6a5m3vxm3cy.eu-west-1.es.amazonaws.com\nx-amz-date:XXXXXXXXXXXX\n\nhost;x-amz-date\n271d5ef919251148dc0b5b3f3968c3debc911a41b60ef4e92c55b98057d6cdd4'\n\nThe String-to-Sign should have been\n'AWS4-HMAC-SHA256\XXXXXXXXXXXX\n20170511/eu-west-1/es/aws4_request\n0bd34812e0727fba7c54068b0ae1114db235cfc2f97059b88be43e8b264e1d57'\n"}
This tweak only necessary for the users who are still using Amazon SDK PHP version 2. In version 3, it supported by default.
For signed request i updated the current elsticsearch client handler by adding a middle ware for signing the request.
$elasticConfig = Configure::read('ElasticSearch');
$middleware = new AwsSignatureMiddleware();
$defaultHandler = \Elasticsearch\ClientBuilder::defaultHandler();
$awsHandler = $middleware($defaultHandler);
$clientBuilder = \Elasticsearch\ClientBuilder::create();
$clientBuilder->setHandler($awsHandler)
->setHosts([$elasticConfig['host'].':'.$elasticConfig['port']]);
$client = $clientBuilder->build();
I used the following library for this purpose
use Aws\Common\Credentials\CredentialsInterface;
use Aws\Common\Signature\SignatureInterface;
use Guzzle\Http\Message\Request;
class AwsSignatureMiddleware
{
/**
* #var \Aws\Credentials\CredentialsInterface
*/
protected $credentials;
/**
* #var \Aws\Signature\SignatureInterface
*/
protected $signature;
/**
* #param CredentialsInterface $credentials
* #param SignatureInterface $signature
*/
public function __construct()
{
$amazonConf = Configure::read('AmazonSDK');
$this->credentials = new \Aws\Common\Credentials\Credentials($amazonConf['key'], $amazonConf['secret']);
$this->signature = new \Aws\Common\Signature\SignatureV4('es', 'eu-west-1');
}
/**
* #param $handler
* #return callable
*/
public function __invoke($handler)
{
return function ($request) use ($handler) {
$headers = $request['headers'];
if ($headers['host']) {
if (is_array($headers['host'])) {
$headers['host'] = array_map([$this, 'removePort'], $headers['host']);
} else {
$headers['host'] = $this->removePort($headers['host']);
}
}
if (!empty($request['body'])) {
$headers['x-amz-content-sha256'] = hash('sha256', $request['body']);
}
$psrRequest = new Request($request['http_method'], $request['uri'], $headers);
$this->signature->signRequest($psrRequest, $this->credentials);
$headerObj = $psrRequest->getHeaders();
$allHeaders = $headerObj->getAll();
$signedHeaders = array();
foreach ($allHeaders as $header => $allHeader) {
$signedHeaders[$header] = $allHeader->toArray();
}
$request['headers'] = array_merge($signedHeaders, $request['headers']);
return $handler($request);
};
}
protected function removePort($host)
{
return parse_url($host)['host'];
}
}
The exact line i tweaked for this purpose is
if (!empty($request['body'])) {
$headers['x-amz-content-sha256'] = hash('sha256', $request['body']);
}
For PUT and POST request the payload hash was wrong because i was not considering the request body while generating payload.
Hope this code is beneficial for anyone who is using Amazon SDK PHP version 2 and using the IAM based authentication for Elasticsearch Hosted service in Amazon cloud.

Categories