microsoftgraph / msgraph-sdk-php

Microsoft Graph Library for PHP.
Other
553 stars 141 forks source link

Fatal error: Uncaught League\OAuth2\Client\Provider\Exception\IdentityProviderException: invalid_request #1472

Open tvu-workcentrix opened 4 months ago

tvu-workcentrix commented 4 months ago

Hi,

I'm not sure if this the right place to ask for help but I have to try:

I already received and stored access and refresh tokens through the OAuth2 authorization code grant flow. Now I'm using this access token and OnBehalfOfContext (like described in one of the examples) to read my profile, just to see if everything is working. But I get a fatal error :invalid_request (see title).

I can manage to read my profile with this SDK when I use AuthorizationCodeContext. Unfortunately this won't be of use in my use cases that I want to implement.

Kind regards

spggooss commented 2 months ago

I want to add the specific error to this issue: AADSTS50027: JWT token is invalid or malformed.

This is correct as the received AccessToken from the AuthorizationCodeContext is not (always?) a JWT. This also creates errors in the setCacheKey function in DelegatedPermissionTrait as described in: https://github.com/microsoftgraph/msgraph-sdk-php/issues/1407

For my use case I need to fetch data from a user's calendar in a background service, so asking the user to login to update the data is not possible. For now I can implement this using cURL requests to the endpoints itself but using the SDK would be beter because of automatically handling pagination and model mapping.

mdespeuilles commented 1 month ago

I have the same problem. @tvu-workcentrix, Have you found a solution to this problem?

spggooss commented 1 month ago

Hi, @mdespeuilles, if you look at https://github.com/microsoftgraph/msgraph-sdk-php/issues/1407 there is a proposed temporary solution. I also added the scopes that the graphserviceclient is for:

<?php

namespace App\Services\Microsoft;

use App\Models\AuthToken;
use Illuminate\Support\Facades\Log;
use League\OAuth2\Client\Token\AccessToken;
use Microsoft\Graph\Core\Authentication\GraphPhpLeagueAccessTokenProvider;
use Microsoft\Graph\Core\Authentication\GraphPhpLeagueAuthenticationProvider;
use Microsoft\Graph\GraphServiceClient;
use Microsoft\Kiota\Authentication\Cache\AccessTokenCache;
use Microsoft\Kiota\Authentication\Oauth\AuthorizationCodeContext;

class GraphFactory {
    public static function create(string $accessToken, array $scopes): GraphServiceClient {
        $token = new AccessToken([
            'access_token' => $access_token,
            'token_type' => 'Bearer',
            'expires' => time() + 10, //Our AccessTokenProvider makes sure the token is valid for at least 60 seconds
        ]);

        $tokenRequestContext = new class extends AuthorizationCodeContext {
            public function __construct() {
                //We don't want Microsoft\Graph to request access tokens itself, but all these values may not be empty:
                parent::__construct('x', 'x', 'x', 'x', 'x');
            }

            public function getCacheKey(): ?string {
                return 'ignored'; //this ends up as $identity in AccessTokenCache::getAccessToken(), which we don't use
            }
        };

        $cache = new readonly class($token) implements AccessTokenCache {

            public function __construct(private AccessToken $token) {
            }

            public function getAccessToken(string $identity): ?AccessToken {
                return $this->token;
            }

            public function persistAccessToken(string $identity, AccessToken $accessToken): void {
                Log::warning('Microsoft\Graph trying to persist access token!');
            }
        };

        return GraphServiceClient::createWithAuthenticationProvider(
            GraphPhpLeagueAuthenticationProvider::createWithAccessTokenProvider(
                GraphPhpLeagueAccessTokenProvider::createWithCache($cache, $tokenRequestContext, $scopes)
            )
        );
    }
}
mdespeuilles commented 1 month ago

Thanks @spggooss

daverdalas commented 1 month ago

Here's how I solved this problem by creating my own class that extends BaseSecretContext and sends refresh_token as a grant type:

<?php

use Microsoft\Kiota\Authentication\Oauth\BaseSecretContext;
use Microsoft\Kiota\Authentication\Oauth\DelegatedPermissionTrait;
use Microsoft\Kiota\Authentication\Oauth\TokenRequestContext;

class OnBehalfOfContextUsingRefreshToken extends BaseSecretContext implements TokenRequestContext
{
    use DelegatedPermissionTrait;

    public function __construct(
        string $tenantId,
        string $clientId,
        string $clientSecret,
        private readonly string $assertion,
        private readonly array $additionalParams = []
    ) {
        if (! $assertion) {
            throw new \InvalidArgumentException("Assertion cannot be empty");
        }

        parent::__construct($tenantId, $clientId, $clientSecret);
    }

    public function getParams(): array
    {
        return array_merge($this->additionalParams, parent::getParams(), [
            'refresh_token' => $this->assertion,
            'grant_type' => $this->getGrantType(),
        ]);
    }

    public function getGrantType(): string
    {
        return 'refresh_token';
    }
}

and to use it:

$tokenRequestContext = new OnBehalfOfContextUsingRefreshToken(
  tenantId: $tenantId,
  clientId: $clientId,
  clientSecret: $clientSecret,
  assertion: $refreshToken,
);

$client = new GraphServiceClient($tokenRequestContext, $scope);