Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
knobel-dk opened this issue Sep 21, 2024 · 2 comments
Closed
Labels
no-recent-activity status:waiting-for-author-feedback Issue that we've responded but needs author feedback to close type:question An issue that's a question

Comments

@knobel-dk
Copy link

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');
    }
}
@knobel-dk knobel-dk added the status:waiting-for-triage An issue that is yet to be reviewed or assigned label Sep 21, 2024
@Ndiritu
Copy link
Contributor

Ndiritu commented Sep 30, 2024

@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.

@Ndiritu Ndiritu added status:waiting-for-author-feedback Issue that we've responded but needs author feedback to close type:question An issue that's a question and removed status:waiting-for-triage An issue that is yet to be reviewed or assigned labels Sep 30, 2024
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
no-recent-activity status:waiting-for-author-feedback Issue that we've responded but needs author feedback to close type:question An issue that's a question
Projects
None yet
Development

No branches or pull requests

2 participants