microsoftgraph / microsoftgraph/msgraph-sdk-php
invalid_request: Unexpected client assertion type, when using certificates based authorization
Nobody has claimed this yet.
- Dominant language
- PHP
- Stars
- 669
- Forks
- 150
- Avg merge
- 15h 21m
- Merged PRs (30d)
- 3
Description
Describe the bug
Hello, I'm trying to use certificates (self-signed) based authorization for getting access tokens on behalf of user (their outlook calendar resources).
I'm using AuthorizationCodeCertificateContext to acquire initial access and refresh tokens, then I store them in db for later usage.
Using stored access token works well until it expires, when I get invalid_request error when sdk tries to refresh access token, again using stored refresh token
I'm not sure how am I able to get access and refresh tokens using AuthorizationCodeCertificateContext, then use access token successfully to fetch some user resources, but not able to refresh access token.
Expected behavior
If I understand token management correctly, expected behavior should be that refreshing token just works out of the box if I provide refresh token to InMemoryAccessTokenCache.
I tried same thing using secrets instead of certificates, it works as expected.
Also checked if sdk actually is trying to refresh token and is sending proper request, it should be okay as well:
method: POST
url: https://login.microsoftonline.com/{my-tenant-id}/oauth2/v2.0/token
headers: [
"content-type" => "application/x-www-form-urlencoded"
]
body:
"client_id" => "abc"
"grant_type" => "refresh_token"
"client_assertion" => "eyJ0eXAiOiJKV1QiLCJ4NXQiOiJXUklac2RPRE9WaHlxTWFoQ2VfYUxnTHdGTE0iLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vZTU0OGZkOWQtZmJmZC0..."
"client_assertion_type" => "urn:ietf:params:Oauth:client-assertion-type:jwt-bearer"
"refresh_token" => "1.AR8Anf1I5f372U6UbFh_vPA_wn8-2g2i6gpHmbfGx5rrSzccAfUfAA.AgABAwEAAABVrSpeuWamRam2jAF1XRQEAwDs_..."
How to reproduce
this is how I created self-signed CA:
$privateKeyOptions = [
'digest_alg' => OPENSSL_ALGO_SHA256,
'private_key_bits' => 2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
];
// Generate a new private key
$privateKey = openssl_pkey_new($privateKeyOptions);
if ($privateKey === false) {
throw new RuntimeException(sprintf('Failed to generate private key: %s', openssl_error_string()));
}
// Generate a certificate signing request
$csr = openssl_csr_new([
'countryName' => 'xx',
'stateOrProvinceName' => 'xxxx',
'localityName' => 'xx',
'organizationName' => 'xx',
'commonName' => 'localhost',
], $privateKey, $privateKeyOptions);
if (is_bool($csr)) {
throw new RuntimeException(sprintf('Failed to open RSA key: %s', openssl_error_string()));
}
// Generate a self-signed certificate valid for 1 year
$certificate = openssl_csr_sign(
csr: $csr,
ca_certificate: null,
private_key: $privateKey,
days: CertificateRotatorInterface::MAX_DURATION_IN_DAYS
);
if ($certificate === false) {
throw new RuntimeException(sprintf('Failed to sign data: %s', openssl_error_string()));
}
// Export the certificate and key to files
$certificateFilePath = '/tmp/tls_crt.pem';
$privateKeyFilePath = '/tmp/tls_key.pem';
if (!openssl_x509_export_to_file($certificate, $certificateFilePath)) {
throw new RuntimeException(sprintf('Failed to export certificate: %s', openssl_error_string()));
}
if (!openssl_pkey_export_to_file($privateKey, $privateKeyFilePath)) {
throw new RuntimeException(sprintf('Failed to export private key: %s', openssl_error_string()));
}
tls_crt.pem file is uploaded through Microsoft Entra Admin to an application I registered for testing.
GraphServiceClient setup:
(I use "code_verifier" while getting authorization code, here it's just some placeholder value)
$tokenRequestContext = new AuthorizationCodeCertificateContext(
tenantId: $tenantId,
clientId: $clientId,
authCode: 'default',
redirectUri: $redirectUri,
certificatePath: $certificatePath,
privateKeyPath: $privateKeyPath,
additionalParams: ['code_verifier' => 'default']
);
$inMemoryCache = new InMemoryAccessTokenCache(
$tokenRequestContext,
new AccessToken([
'access_token' => $token['accessToken'],
'refresh_token' => $token['refreshToken'],
'expires' => $token['expires'],
])
);
$graphServiceClient = GraphServiceClient::createWithAuthenticationProvider(
GraphPhpLeagueAuthenticationProvider::createWithAccessTokenProvider(
GraphPhpLeagueAccessTokenProvider::createWithCache(
$accessTokenCache,
$tokenRequestContext,
$scopes
)
)
);
dump($graphServiceClient->me()->get()->wait());
SDK Version
2.30.0
Latest version known to work for scenario above?
No response
Known Workarounds
No response
Debug output
League\OAuth2\Client\Provider\Exception\IdentityProviderException {
#response: array:6 [
"error" => "invalid_request"
"error_description" => "AADSTS90023: Unexpected client assertion type. Trace ID: a0dfae09-fd6a... Correlation ID: ae87cdb5-9cb1-4a43... Timestamp: 2025-03-24 14:15:17Z "
"error_codes" => array:1 [
0 => 90023
]
"timestamp" => "2025-03-24 14:15:17Z"
"trace_id" => "a0dfae09-fd6a-482f..."
"correlation_id" => "ae87cdb5-9cb1-4a43..."
]
}
Configuration
docker base image: php:8.4-fpm-alpine3.21
Other information
No response
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with AuthorizationCodeCertificateContext, InMemoryAccessTokenCache, and GraphPhpLeagueAccessTokenProvider, then trace the refresh-token request shown in the report. Compare certificate-based refresh behavior with the working secret-based flow; the issue is resolved when the stored refresh token can refresh the access token without AADSTS90023.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- php
- Domain
- api, authentication
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100