microsoftgraph / msgraph-sdk-php

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

Breaking changes from 2.0-RC4 to 2.14: Assertion failed signature validation #1585

Closed knobel-dk closed 2 weeks ago

knobel-dk commented 1 month ago

The repo has very few examples and no upgrade guide. A few issues here wonder where Microsoft\Graph\Graph went.

I tried figuring out myself and am pretty sure that this is a breaking change, if not a bug or undocumented behavior.

This code worked on 2.0-RC4:

<?php

namespace Support\Extensions\Auth\AzureActiveDirectory\Actions;

use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Provider\GenericProvider;
use Microsoft\Graph\Graph;
use Support\Extensions\Auth\AzureActiveDirectory\Dtos\TenantDto;

class AuthenticateToTenantAction
{
    private GenericProvider $oauthClient;

    private Graph $graph;

    private string $authUrl;

    public function __construct(TenantDto $tenantDto)
    {
        $this->oauthClient = new GenericProvider([
            'clientId' => $tenantDto->clientId,
            'clientSecret' => $tenantDto->clientSecret,
            'redirectUri' => $tenantDto->redirectUri,
            'urlAuthorize' => $tenantDto->urlAuthorize,
            'urlAccessToken' => $tenantDto->urlAccessToken,
            'scopes' => $tenantDto->scopes,
            'urlResourceOwnerDetails' => $tenantDto->urlResourceOwnerDetails,
        ]);

        $this->authUrl = $this->oauthClient->getAuthorizationUrl();

        $this->graph = new Graph();
    }

    public function getAuthUrl(): string
    {
        return $this->authUrl;
    }

    public function getState(): string
    {
        return $this->oauthClient->getState();
    }

    public function getUser(string $authCode)
    {
        try {
            $token = $this->oauthClient->getAccessToken('authorization_code', [
                'code' => $authCode,
            ]);
        } catch (IdentityProviderException $e) {
            throw new \Exception('IdentityProviderException: '.$e->getMessage().' '.json_encode($e->getResponseBody()));
        }

        $this->graph->setAccessToken($token->getToken());
        $user = $this->graph->createRequest('GET', '/me?$select=displayName,mail')
            ->execute()->getBody();

        return [$token, $user];
    }
}

Then I upgraded to 2.14 and tried this (kindly look at the two comments):

<?php

namespace Support\Extensions\Auth\AzureActiveDirectory\Actions;

use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Provider\GenericProvider;
use Microsoft\Graph\Generated\Users\Item\UserItemRequestBuilderGetRequestConfiguration;
use Microsoft\Graph\GraphServiceClient;
use Microsoft\Kiota\Authentication\Oauth\OnBehalfOfContext;
use Support\Extensions\Auth\AzureActiveDirectory\Dtos\TenantDto;

class AuthenticateToTenantAction
{
    private GenericProvider $oauthClient;

    private string $authUrl;

    public function __construct(private TenantDto $tenantDto)
    {
        $this->oauthClient = new GenericProvider([
            'clientId' => $tenantDto->clientId,
            'clientSecret' => $tenantDto->clientSecret,
            'redirectUri' => $tenantDto->redirectUri,
            'urlAuthorize' => $tenantDto->urlAuthorize,
            'urlAccessToken' => $tenantDto->urlAccessToken,
            'scopes' => $tenantDto->scopes,
            'urlResourceOwnerDetails' => $tenantDto->urlResourceOwnerDetails,
        ]);

        $this->authUrl = $this->oauthClient->getAuthorizationUrl();
    }

    public function getAuthUrl(): string
    {
        return $this->authUrl;
    }

    public function getState(): string
    {
        return $this->oauthClient->getState();
    }

    public function getUser(string $authCode)
    {
        try {
            $token = $this->oauthClient->getAccessToken('authorization_code', [
                'code' => $authCode,
            ]);
        } catch (IdentityProviderException $e) {
            throw new \Exception('IdentityProviderException: '.$e->getMessage().' '.json_encode($e->getResponseBody()));
        }

        $tokenRequestContext = new OnBehalfOfContext(
            tenantId: 'common', # <-- I did not need that in my old code?
            clientId: $this->tenantDto->clientId,
            clientSecret: $this->tenantDto->clientSecret,
            assertion: $token->getToken(),
        );

        $requestConfiguration = new UserItemRequestBuilderGetRequestConfiguration();
        $queryParameters = UserItemRequestBuilderGetRequestConfiguration::createQueryParameters();
        $queryParameters->select = ['displayName','mail'];
        $requestConfiguration->queryParameters = $queryParameters;

        $graphServiceClient = new GraphServiceClient($tokenRequestContext, explode(' ', $this->tenantDto->scopes));
        $user = $graphServiceClient->me()->get($requestConfiguration)->wait(); # <-- This throw the exception below

        return [$token, $user];
    }
}

It gives Assertion failed signature validation


{#2307 ▼ // src/Support/Extensions/Auth/AzureActiveDirectory/Actions/AuthenticateToTenantAction.php:62
  -exception:
League\OAuth2\Client\Provider\Exception
\
IdentityProviderException {#2304 ▼
    #message: "invalid_grant"
    #code: 0
    #file: "
/var/www/html/vendor
/league/oauth2-client/
src/Provider/GenericProvider.php"
    #line: 222
    #response: array:7 [▼
      "error" => "invalid_grant"
      "error_description" => "
AADSTS50013: Assertion failed signature validation. [Reason - Key was found, but use of the key to verify the signature failed., Thumbprint of key used by client: '1FD9E3E40392B30329860D52171EE3695FA507DC', Found key 'Start=08/18/2024 19:33:23, End=08/18/2029 19:33:23', Please visit the Azure Portal, Graph Explorer or directly use MS Graph to see configured keys for app Id '00000000-0000-0000-0000-000000000000'. Review the documentation at https://docs.microsoft.com/en-us/graph/deployments to determine the corresponding service endpoint and https://docs.microsoft.com/en-us/graph/api/application-get?view=graph-rest-1.0&tabs=http to build a query request URL, such as 'https://graph.microsoft.com/beta/applications/00000000-0000-0000-0000-000000000000']. Trace ID: 779c7e83-1547-45d7-84e9-1dc190a70a00 Correlation ID: 6577dee5-4675-4cac-875d-7faa7929c1ef Timestamp: 2024-09-21 16:34:51Z
 ◀
"
      "error_codes" => array:1 [▶]
      "timestamp" => "2024-09-21 16:34:51Z"
      "trace_id" => "779c7e83-1547-45d7-84e9-1dc190a70a00"
      "correlation_id" => "6577dee5-4675-4cac-875d-7faa7929c1ef"
      "error_uri" => "https://login.microsoftonline.com/error?code=50013"
    ]

Here is the controller consuming `` for both versions:

<?php

namespace Support\Extensions\Auth\AzureActiveDirectory\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use Support\Extensions\Auth\AzureActiveDirectory\Actions\AuthenticateToTenantAction;
use Support\Extensions\Auth\AzureActiveDirectory\Actions\FindUserAction;
use Support\Extensions\Auth\AzureActiveDirectory\Actions\PersistTokenAction;
use Support\Extensions\Auth\AzureActiveDirectory\Actions\SignInUserAction;
use Support\Extensions\Auth\AzureActiveDirectory\Dtos\TenantDto;
use Support\Extensions\Auth\AzureActiveDirectory\Models\TenantModel;

class AzureActiveDirectoryAuthController extends Controller
{
    public function signin(TenantModel $tenant)
    {
        $authenticateTenantAction = new AuthenticateToTenantAction(
            TenantDto::fromArray($tenant->toArray())
        );
        session(['oauthState' => $authenticateTenantAction->getState()]);

        return redirect()->away($authenticateTenantAction->getAuthUrl());
    }

    public function callback(Request $request, TenantModel $tenant)
    {
        $expectedState = $request->session()->pull('oauthState', null);
        if (is_null($expectedState)) {
            return redirect('/login')->with(['error' => 'No state sent to Microsoft']);
        }

        $actualState = $request->query('state');
        if (is_null($actualState) || $expectedState !== $actualState) {
            return redirect('/login')->with(['error' => 'Microsoft returned a different state than expected.']);
        }

        $authenticateTenantAction = new AuthenticateToTenantAction(
            TenantDto::fromArray($tenant->toArray())
        );

        try {
            if (is_null($request->query('code'))) {
                return redirect('/login')->with(['error' => 'Microsoft login did not return a code. Did you use your work email?']);
            }

            [$token, $userResponse] = $authenticateTenantAction->getUser($request->query('code'));

            $user = FindUserAction::for($userResponse, $tenant);

            SignInUserAction::for($user);

            $token = PersistTokenAction::for($user, $token);
        } catch (ModelNotFoundException $e) {
            report('Azure AD Error: We tried finding this AD user in our users table but got ModelNotFoundException: '.json_encode($userResponse));

            return redirect('/login')->with(['error' => 'Your admin should create you in our system first.']);
        } catch (IdentityProviderException $e) {
            $response = is_string($e->getResponseBody()) ? json_decode($e->getResponseBody(), true) : $e->getResponseBody();
            $message = $response['error_description'] ?? $e->getResponseBody();
            report("Azure AD Error: $message");

            return redirect('/login')->with(['error' => $message]);
        } catch (\Exception $e) {
            report($e);

            return redirect('/login')->with(['error' => $e->getMessage()]);
        }

        return redirect('/login');
    }
}
Ndiritu commented 3 weeks ago

@knobel-dk Thank you for upgrading to the latest SDK and reaching out. Yes, there are some expected breaking changes between the RC and the stable versions.

For your scenario, you'll need to use the AuthorizationCodeContext since you're using the OAuth 2.0 authorization code flow to authenticate. See this for more.

The latest SDK is able to do the token request for you when given an authorization code. It also refreshes this access token when needed and caches it in memory. You can also choose to pass in an already retrieved access token to the GraphServiceClient - more

We also provide an Upgrade Guide with more samples and features.

Feel free to reach out in case of any further challenges.

microsoft-github-policy-service[bot] commented 3 weeks ago

This issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for 4 days. It will be closed if no further activity occurs within 3 days of this comment.