0
votes

There is a .NET Framework-based Windows desktop application which communicates with a PHP-based backend web site. I am experiencing permanent signature validation failures when trying to validate the tokens provided by the .NET application to the PHP web site (openssl_verify returns 0).

  1. .NET Framework application uses a Microsoft Authentication Library (MSAL) to authenticate a user against an Azure Active Directory (AAD). The library returns a token as a string. The application sends this token as part of its request to the PHP-based server.

    var result = await Application.AcquireTokenAsync(scopes).ConfigureAwait(false);

    var token = result.AccessToken;

    headers.Add("X-Auth-AAD-Token", token);

  2. The PHP-based server web site receives the token provided by the .NET application. It than tries to verify the token and use it when requesting user data from an Azure Active Directory. The PHP web site uses Networg/oauth2-azure library which is a provider of the generic thephpleage/oauth2-client library which then uses a firebase/php-jwt library to handle JWT tokens.

The PHP application instantiates an Azure provider and calls

$provider->validateAccessToken($token);

where $token is the string received from the .NET application. This method calls

$keys = $this->getJwtVerificationKeys();
(array)JWT::decode($accessToken, $keys, ['RS256'])

where $keys are an array of public keys retrieved from the https://login.windows.net/common/discovery/keys endpoint.

The JWT::decode then split the token to a header, payload and a signature, decode them, choose the right public key and verify the signature:

public static function decode($jwt, $key, array $allowed_algs = array())
    $tks = explode('.', $jwt);
    list($headb64, $bodyb64, $cryptob64) = $tks;
    $header = static::jsonDecode(static::urlsafeB64Decode($headb64))
    $sig = static::urlsafeB64Decode($cryptob64);
    $key = $key[$header->kid];

    static::verify("$headb64.$bodyb64", $sig, $key, $header->alg);

where jsonDecode calls

$obj = json_decode($input, false, 512, JSON_BIGINT_AS_STRING);

and urlsafeB64Decode looks like

public static function urlsafeB64Decode($input)
    $remainder = strlen($input) % 4;
    if ($remainder) {
        $padlen = 4 - $remainder;
        $input .= str_repeat('=', $padlen);
    }
    return base64_decode(strtr($input, '-_', '+/'));

The verify method then tries to verify the signature by calling openssl_verify.

private static function verify($msg, $signature, $key, $alg)
    list($function, $algorithm) = static::$supported_algs[$alg]; // list('openssl', 'SHA256')
    openssl_verify($msg, $signature, $key, $algorithm);

The openssl_verify function returns 0 which means that the signature verification failed (not match).

What am I doing wrong? How to fix it?

Edit: I am not supposed to verify the signature unless the token is issued for "me". As the token I checked is scoped to a Graph API, only the Graph API is supposed to verify it. After I changed the scope of the token requested to the web application, the signature verifies as expected.

1

1 Answers

1
votes

In the decode function, the statement $key = $key[$header->kid]; returns the key that corresponds to the kid header parameter you got from https://login.windows.net/common/discovery/keys.

As the keys provided at the url hereabove are under the JWK formt, this statement will return you something like:

{
"kty": "RSA",
"use": "sig",
"kid": "-sxMJMLCIDWMTPvZyJ6tx-CDxw0",
"x5t": "-sxMJMLCIDWMTPvZyJ6tx-CDxw0",
"n": "rxlPnqW6fNuCbdrhDEzwGJVux3iPvtt_8r-uHHIKa7C_b_ux5hewNMS91SgUPZOrsqb54uHj_7INWKqKEtFc4YP83Fhss_uO_mT97czENs4zWaSN9Eww_Fz36xq_uZ65750lHKwXQJ1A_pe-VOgNlPg8ECi7meQDJ05r838eu1jpKFjxkQrdRFTLgYtRQ7TxX-zzRyoRR8iqJc6Rvnijh19-YfWtBsCI1r127SFakUBrY_ZKsKyE9KNWUL7H65EyFRNgK80XfYvhQlGw3-Ajf28fi71wW-BypK1bTCArzwX7zgF3H6P1u8PKosSOSN_Q9-Qc9X-R_Y-3bOpOIiLOvw",
"e": "AQAB",
"x5c": [
"MIIDBTCCAe2gAwIBAgIQKOfEJNDyDplBSXKYcM6UcjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE4MTIyMjAwMDAwMFoXDTIwMTIyMjAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK8ZT56lunzbgm3a4QxM8BiVbsd4j77bf/K/rhxyCmuwv2/7seYXsDTEvdUoFD2Tq7Km+eLh4/+yDViqihLRXOGD/NxYbLP7jv5k/e3MxDbOM1mkjfRMMPxc9+sav7meue+dJRysF0CdQP6XvlToDZT4PBAou5nkAydOa/N/HrtY6ShY8ZEK3URUy4GLUUO08V/s80cqEUfIqiXOkb54o4dffmH1rQbAiNa9du0hWpFAa2P2SrCshPSjVlC+x+uRMhUTYCvNF32L4UJRsN/gI39vH4u9cFvgcqStW0wgK88F+84Bdx+j9bvDyqLEjkjf0PfkHPV/kf2Pt2zqTiIizr8CAwEAAaMhMB8wHQYDVR0OBBYEFC//HOy7pEIKtnpMj4bEMA3oJ39uMA0GCSqGSIb3DQEBCwUAA4IBAQAIYxZXIpwUX8HjSKWUMiyQEn0gRizAyqQhC5wdWOFCBIZPJs8efOkGTsBg/hA+X1fvN6htcBbJRfFfDlP/LkLIVNv2zX4clGM20YhY8FQQh9FWs5qchlnP4lSk7UmScxgT3a6FG3OcLToukNoK722Om2yQ1ayWtn9K82hvZl5L3P8zYaG1gbHPGW5VlNXds60jIpcSWLdU2hacYmwz4pPQyvNOW68aK/Y/tWrJ3DKrf1feDbmm7O5kpWVYWRpah+i6ePjELNkc2Jr+2DchBQTIh9Fxe8sz+9iOyLh9tubMJ+7RTs/ksK0sQ1NVScGFxK+o5hFOOMK7y/F5r467jHez"
]
}

OpenSSl cannot directly use a JWK. You must convert it into a PEM or DER public key file. Hopefully, this key format is already included in the JWK itself: it is included in the first X.509 certificate in the x5c parameter.

You just have to

  • Modify it a bit to get a valid X.509 certificate.
$certificate = '-----BEGIN CERTIFICATE----'.PHP_EOL;
$certificate .= chunk_split($key['x5c'][0], 64, PHP_EOL);
$certificate .= '-----END CERTIFICATE-----'.PHP_EOL;
  • Get the public key from this certificate
$publicKey = openssl_pkey_get_public($certificate);
  • Use that public key with the openssl_verifyfunction
static::verify("$headb64.$bodyb64", $sig, $publicKey, $header->alg);