Azure AD: Implementing OAuth 2.0 Authorization Code Flow with PKCE in PHP code.

Prashanth Kumar
7 min readMar 14, 2025

--

Problem Statement:

Recently, I was asked to migrate some of my old PHP code from on-premises systems to Azure Cloud. When moving traditional code, there are many challenges in terms of security, authorization (Auth-Z), and authentication (Auth-N). One of the major hurdles I faced was integrating Azure Active Directory (AAD) authentication into a PHP application. Additionally, I encountered issues with SSL certificate validation. Some common errors included ‘Curl error: SSL certificate problem: unable to get local issuer certificate,’ which prevented my application from communicating securely over the internet.

Challenges Faced:

  1. DMZ Architecture vs. Public Facing Architecture: In an organization with a DMZ architecture, such as VNet-based architecture versus public-facing architecture in Azure, external-facing subscriptions may not have integrations with Azure Active Directory, unlike VNet-based subscriptions which have direct integrations. The challenge was how to integrate identity for authentication and authorization.
  2. SSL Certificate Verification Issue: The cURL library, used for making HTTP requests in PHP, was unable to validate the SSL certificate of the Azure endpoint because it couldn’t find the correct Certificate Authority (CA) certificates on my local machine. As a result, I was forced to disable SSL certificate verification to proceed with development, which is insecure and not recommended for production environments.
  3. Mismatch Between SSL Certificate and Private Key: The application was using a private key and certificate for client authentication in Azure. However, I faced issues where the cURL setup failed to recognize the certificate, which could be due to the format or incorrect paths specified for the certificate and private key.
  4. Inability to Achieve a Secure and Reliable Connection: While disabling SSL verification (CURLOPT_SSL_VERIFYPEER = false) allowed me to bypass the errors and proceed with testing, it was clear that this approach was not safe for a production environment. Ensuring the secure exchange of authentication tokens between my PHP app and Azure was critical.

Solution:

Azure AD App registration

To start using Azure AD-provided authentication, follow these steps:

  1. Create a new app registration in your AAD.
  2. Add credentials with a certificate. Follow this article: Quickstart: Register an app in Microsoft Entra ID — Microsoft identity platform.

3. Make sure to add a redirect URI.

Generating Assertion and JWT Token for Auth-Z and Auth-N

  1. First, download the app-registered .pfx file and convert it to .pem format so that your application can read and generate an assertion token for authentication.
  2. Prepare for the token request. Build a query with the help of Postman.
$codeVerifier = $_SESSION['code_verifier'];
$tokenUrl = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token";
$headers = [
"sec-fetch-site: cross-site",
"sec-fetch-mode: cors",
"content-type: application/x-www-form-urlencoded"
];

3. Generate a JWT assertion token for client authentication. Here is the script where I am passing my converted .pem file and certificate thumbprint.

$privateKey = file_get_contents('C:\apps\repos\AzureAppGenerated.pem');
$certThumbprint = "123456789348ABCDEFGHIJBF96DC063123456789";
$thumbprintBytes = hex2bin($certThumbprint);
$thumbprintBytesEncoded = base64_encode($thumbprintBytes);
$thumbprintB64 = str_replace(['+', '/', '='], ['-', '_', ''], $thumbprintBytesEncoded);

$jwtHeader = json_encode([
'alg' => 'RS256',
'typ' => 'JWT',
'x5t' => $thumbprintB64
]);

$jwtPayload = json_encode([
'aud' => "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token",
'iss' => $clientId,
'sub' => $clientId,
'jti' => bin2hex(random_bytes(16)),
'nbf' => time(),
'exp' => time() + 3600
]);

$headerEncoded = base64_encode($jwtHeader);
$payloadEncoded = base64_encode($jwtPayload);
$base64UrlHeader = str_replace(['+', '/', '='], ['-', '_', ''], $headerEncoded);
$base64UrlPayload = str_replace(['+', '/', '='], ['-', '_', ''], $payloadEncoded);
$signatureInput = $base64UrlHeader . '.' . $base64UrlPayload;
$signature = '';
openssl_sign($signatureInput, $signature, $privateKey, OPENSSL_ALGO_SHA256);
$base64UrlSignature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($signature));
$jwtAssertion = $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature;

to build this query took some of the references from this Old Stackoverflow article:

4. Send the token request.

$postFields = [
'client_id' => $clientId,
'grant_type' => 'client_credentials',
'code_challenge_method' => 'S256',
'code' => $_GET['code'],
'code_verifier' => $codeVerifier,
'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
'client_assertion' => $jwtAssertion,
'scope' => $scopes,
'redirect_uri' => $redirectUri
];

$ch = curl_init();
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_URL, $tokenUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postFields));

$response = curl_exec($ch);

5. Finally, handle the response to process and retrieve the access token.

if ($response === false) {
exit('Curl error: ' . curl_error($ch));
} else {
$response_data = json_decode($response, true);
if (isset($response_data['access_token'])) {
exit('Access Token: ' . $response_data['access_token']);
} else {
exit('Error: ' . $response_data['error_description']);
}
}

curl_close($ch);

$tokenData = json_decode($response, true);

if (isset($tokenData['access_token'])) {
$_SESSION['access_token'] = $tokenData['access_token'];
header('Location: ssoprofile.php');
exit;
} else {
exit('Failed to get access token');
}

Once you run your main PHP code for authentication, you will get something like this.

Additional Points to Consider

1. Ensuring SSL Certificate Validity:
Ensure the certificate is valid before you use it in your code. Make sure to run it on your local machine.

2. Configuring cURL to Use the Correct CA Certificates:
Save the certificate in the correct location and configure cURL to use it for SSL certificate verification by setting the CURLOPT_CAINFO option to point to the downloaded file.

curl_setopt($ch, CURLOPT_CAINFO, 'C:\apps\repos\AzureAppGenerated.pem'); 

3. Configuring the Private Key and Certificate:
Define your .pem and .key files correctly. I used OpenSSL to convert my .pfx to .pem and .key formats. This step is optional if you are not using the private key variable:

$privateKey = file_get_contents('C:\apps\repos\AzureAppGenerated.pem');

If you are using these variables, configure cURL as follows:

curl_setopt($ch, CURLOPT_SSLCERT, 'C:\apps\repos\AzureAppGenerated.pem');  
curl_setopt($ch, CURLOPT_SSLKEY, 'C:\apps\repos\AzureAppGenerated_privakey.key');

4. Enabling SSL Verification:
This is important for testing. Make sure to set CURLOPT_SSL_VERIFYPEER to true to enable SSL verification for secure communication. This ensures that cURL verifies the Azure endpoint's SSL certificate and protects against man-in-the-middle (MITM) attacks.

curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);

5. Testing the Solution:
Finally, test your PHP code with Azure to obtain the access token.

6. Final Code:
Here is the full working code:

<?php

session_start();

if (!isset($_GET['code'])) {
exit('No code provided');
}

require_once 'DotEnv.php';

(new DotEnv(dirname(__DIR__) . '\.env'))->load();

$clientId = getenv('ClientID');
$redirectUri = getenv('RedirectURI');
$redirectUri = "https://localhost";
$tenantId = getenv('TenantID');
$clientSecret = getenv('ClientSecret');
$scopes = 'https://graph.microsoft.com/.default';
// $scopes = "https://management.core.windows.net/.default"; (you can always try with management scope as well in case if you havent mapped with graph api scope).



$codeVerifier = $_SESSION['code_verifier'];

$tokenUrl = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token";


$headers = [
"sec-fetch-site: cross-site",
"sec-fetch-mode: cors",
"content-type: application/x-www-form-urlencoded"
];



$privateKey = file_get_contents('C:\apps\repos\AzureAppGenerated.pem');
$certThumbprint = "123456789348ABCDEFGHIJBF96DC063123456789";
$thumbprintBytes = hex2bin($certThumbprint);
$thumbprintBytesEncoded = base64_encode($thumbprintBytes);
$thumbprintB64 = str_replace(['+', '/', '='], ['-', '_', ''], $thumbprintBytesEncoded);


$jwtHeader = json_encode([
'alg' => 'RS256',
'typ' => 'JWT',
'x5t' => $thumbprintB64
]);

$jwtPayload = json_encode([
'aud' => "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token",
'iss' => $clientId,
'sub' => $clientId,
'jti' => bin2hex(random_bytes(16)),
'nbf' => time(),
'exp' => time() + 3600
]);

$headerEncoded = base64_encode($jwtHeader);
$payloadEncoded = base64_encode($jwtPayload);

$base64UrlHeader = str_replace(['+', '/', '='], ['-', '_', ''], $headerEncoded);
$base64UrlPayload = str_replace(['+', '/', '='], ['-', '_', ''], $payloadEncoded);

$signatureInput = $base64UrlHeader . '.' . $base64UrlPayload;
$signature = '';
openssl_sign($signatureInput, $signature, $privateKey, OPENSSL_ALGO_SHA256);
$base64UrlSignature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($signature));

$jwtAssertion = $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature;


$postFields = [
'client_id' => $clientId,
'grant_type' => 'client_credentials',
'code_challenge_method' => 'S256',
'code' => $_GET['code'],
'code_verifier' => $codeVerifier,
'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
'client_assertion' => $jwtAssertion,
'scope' => $scopes,
'redirect_uri' => $redirectUri
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);

curl_setopt($ch, CURLOPT_URL, $tokenUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postFields));

$response = curl_exec($ch);

if ($response === false) {
exit( 'Curl error: ' . curl_error($ch));
} else {
$response_data = json_decode($response, true);
if (isset($response_data['access_token'])) {
exit( 'Access Token: ' . $response_data['access_token']);
} else {
exit( 'Error: ' . $response_data['error_description']);
}
}

curl_close($ch);

$tokenData = json_decode($response, true);

if (isset($tokenData['access_token'])) {
$_SESSION['access_token'] = $tokenData['access_token'];
header('Location: ssoprofile.php');
exit;
} else {
exit('Failed to get access token');
}
?>

Conclusion:

By following the steps above, I was able to successfully integrate Azure Active Directory (AAD) authentication into my PHP application while ensuring secure communication through SSL verification.

This solution ensures that communication between the PHP app and Azure remains secure, thus achieving a robust and reliable authentication flow.

Reference links:

--

--

Prashanth Kumar
Prashanth Kumar

Written by Prashanth Kumar

IT professional with 20+ years experience, feel free to contact me at: Prashanth.kumar.ms@outlook.com

No responses yet