microsoftgraph / msgraph-sdk-php

Microsoft Graph Library for PHP.
Other
583 stars 144 forks source link

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

Open tvu-workcentrix opened 9 months ago

tvu-workcentrix commented 9 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 7 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 6 months ago

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

spggooss commented 6 months 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 6 months ago

Thanks @spggooss

daverdalas commented 5 months 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);
armetiz commented 2 months ago

Hi @daverdalas , thank you for the proposed solution! It's working. @Ndiritu could you please check it? Is this something that can be used in production?

I'm also coming from v1, storing by my self access_token and refresh_token.

I searched for several hours for an official solution to use these two tokens as well as the expiration date. But apart from the github issues that offer unofficial solutions, I found nothing conclusive.

I migrated all our dependencies to Microsoft Graph SDK from v1 to v2. And we see that version 2 brings better productivity and secures our developments mainly thanks to typing. But the OAuth authentication part still remains a mystery..

Best regards, Thomas.

tnup commented 2 months ago

Same here, took me hours @armetiz , thank you for posting @daverdalas.

I am not sure however why you are actively using the refresh token and not the accesstoken?

iflow commented 1 month ago

Anyone figured out how to reuse the _accesstoken or a similar solution like that one from @daverdalas without using the _refreshtoken directly?

Ndiritu commented 1 week ago

Hi, thank you for your patience on this issue.

A couple of issues are overlapping here

  1. When using the OnBehalfOfContext, the assertion should not be a previously acquired Microsoft Graph access token. This leads to a failure with invalid_grant etc. I have updated the docs in the linked PR to reflect this. Apologies for the wrong guidance initially here & I recognize that the documentation could improve overall. Here are some quick guides from Microsoft Q&A and StackOverflow that have the steps to expose your API under the app registration & use the access token granted to call Microsoft Graph on behalf of your users after the initial login

  2. The issue with JWT formats and this library assuming all access tokens are JWTs has been fixed here but the release is pending

  3. On the workaround provided by @daverdalas, this technically is a refresh_token grant happening and not an on-behalf-of authentication flow. However I'm considering this as a useful context to expose to allow easy initialising of the client with a refresh token. Especially for cases where the on-behalf-of setup/configuration is too much and your application is ok with re-using a Microsoft Graph access & refresh token from a previous successful login session & initialises the Graph client in future with only the refresh token

cc: @iflow @tnup @armetiz @spggooss @tvu-workcentrix @mdespeuilles