microsoftgraph / microsoftgraph/msgraph-sdk-php

invalid_request: Unexpected client assertion type, when using certificates based authorization

Open
#1,661 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

status:waiting-for-triage type:bug
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.