From ab93d92d583ae3b264a4f943510e0e2c59ee6ee4 Mon Sep 17 00:00:00 2001 From: Mauro Cassani Date: Thu, 30 Apr 2026 16:46:03 +0200 Subject: [PATCH] fix: migrate Microsoft OAuth to v2.0 endpoints and Graph API --- .../Oauth/Microsoft/MicrosoftProvider.php | 69 +++++++++++++------ phpstan-baseline.neon | 42 ----------- 2 files changed, 47 insertions(+), 64 deletions(-) diff --git a/lib/Model/ConnectedServices/Oauth/Microsoft/MicrosoftProvider.php b/lib/Model/ConnectedServices/Oauth/Microsoft/MicrosoftProvider.php index 23a9d8e6ba..dd74bdb06a 100644 --- a/lib/Model/ConnectedServices/Oauth/Microsoft/MicrosoftProvider.php +++ b/lib/Model/ConnectedServices/Oauth/Microsoft/MicrosoftProvider.php @@ -2,13 +2,12 @@ namespace Model\ConnectedServices\Oauth\Microsoft; +use GuzzleHttp\Client; use GuzzleHttp\Exception\GuzzleException; -use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use League\OAuth2\Client\Token\AccessToken; use Model\ConnectedServices\Oauth\AbstractProvider; use Model\ConnectedServices\Oauth\ProviderUser; use Stevenmaguire\OAuth2\Client\Provider\Microsoft; -use Stevenmaguire\OAuth2\Client\Provider\MicrosoftResourceOwner; use Utils\Registry\AppConfig; class MicrosoftProvider extends AbstractProvider @@ -16,6 +15,11 @@ class MicrosoftProvider extends AbstractProvider const string PROVIDER_NAME = 'microsoft'; + private const string AUTHORIZE_URL = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize'; + private const string TOKEN_URL = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'; + private const string RESOURCE_OWNER_URL = 'https://graph.microsoft.com/v1.0/me'; + private const string SCOPES = 'openid email profile User.Read'; + /** * @param string|null $redirectUrl * @@ -27,6 +31,9 @@ public static function getClient(?string $redirectUrl = null): Microsoft 'clientId' => AppConfig::$MICROSOFT_OAUTH_CLIENT_ID, 'clientSecret' => AppConfig::$MICROSOFT_OAUTH_CLIENT_SECRET, 'redirectUri' => $redirectUrl ?? AppConfig::$MICROSOFT_OAUTH_REDIRECT_URL, + 'urlAuthorize' => self::AUTHORIZE_URL, + 'urlAccessToken' => self::TOKEN_URL, + 'urlResourceOwnerDetails' => self::RESOURCE_OWNER_URL, ]); } @@ -37,53 +44,71 @@ public static function getClient(?string $redirectUrl = null): Microsoft */ public function getAuthorizationUrl(string $csrfTokenState): string { - $options = [ + $params = [ + 'client_id' => AppConfig::$MICROSOFT_OAUTH_CLIENT_ID, + 'redirect_uri' => $this->redirectUrl ?? AppConfig::$MICROSOFT_OAUTH_REDIRECT_URL, + 'response_type' => 'code', + 'scope' => self::SCOPES, 'state' => $csrfTokenState, - 'prompt' => 'select_account' + 'prompt' => 'select_account', ]; - $microsoftClient = static::getClient($this->redirectUrl); - - return $microsoftClient->getAuthorizationUrl($options); + return self::AUTHORIZE_URL . '?' . http_build_query($params); } /** * @param string $code * * @return AccessToken - * @throws IdentityProviderException * @throws GuzzleException + * @throws \RuntimeException + * @throws \InvalidArgumentException */ public function getAccessTokenFromAuthCode(string $code): AccessToken { - $microsoftClient = static::getClient($this->redirectUrl); + $httpClient = new Client(); - /** @var AccessToken $token */ - $token = $microsoftClient->getAccessToken('authorization_code', [ - 'code' => $code + $response = $httpClient->post(self::TOKEN_URL, [ + 'form_params' => [ + 'client_id' => AppConfig::$MICROSOFT_OAUTH_CLIENT_ID, + 'client_secret' => AppConfig::$MICROSOFT_OAUTH_CLIENT_SECRET, + 'code' => $code, + 'redirect_uri' => $this->redirectUrl ?? AppConfig::$MICROSOFT_OAUTH_REDIRECT_URL, + 'grant_type' => 'authorization_code', + 'scope' => self::SCOPES, + ], ]); - return $token; + $data = json_decode($response->getBody()->getContents(), true); + + return new AccessToken($data); } /** * @param AccessToken $token * - * @return mixed + * @return ProviderUser * @throws GuzzleException - * @throws IdentityProviderException + * @throws \RuntimeException + * @throws \TypeError */ public function getResourceOwner(AccessToken $token): ProviderUser { - $microsoftClient = static::getClient($this->redirectUrl); - /** @var MicrosoftResourceOwner $fetched */ - $fetched = $microsoftClient->getResourceOwner($token); + $httpClient = new Client(); + + $response = $httpClient->get(self::RESOURCE_OWNER_URL, [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $token->getToken(), + ], + ]); + + $data = json_decode($response->getBody()->getContents(), true); $user = new ProviderUser(); - $user->email = $fetched->getEmail(); - $user->name = $fetched->getFirstname(); - $user->lastName = $fetched->getLastname(); - $user->picture = null; // profile picture is not publicly accessible + $user->email = $data['mail'] ?? $data['userPrincipalName'] ?? null; + $user->name = $data['givenName'] ?? null; + $user->lastName = $data['surname'] ?? null; + $user->picture = null; $user->authToken = $token; $user->provider = self::PROVIDER_NAME; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 76c0ee2002..850d8f046f 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -8712,48 +8712,6 @@ parameters: count: 1 path: lib/Model/ConnectedServices/Oauth/LinkedIn/LinkedInProvider.php - - - message: '#^Method Model\\ConnectedServices\\Oauth\\LinkedIn\\LinkedInProvider\:\:getResourceOwner\(\) throws checked exception TypeError but it''s missing from the PHPDoc @throws tag\.$#' - identifier: missingType.checkedException - count: 5 - path: lib/Model/ConnectedServices/Oauth/LinkedIn/LinkedInProvider.php - - - - message: '#^Method Model\\ConnectedServices\\Oauth\\Microsoft\\MicrosoftProvider\:\:getAccessTokenFromAuthCode\(\) throws checked exception UnexpectedValueException but it''s missing from the PHPDoc @throws tag\.$#' - identifier: missingType.checkedException - count: 1 - path: lib/Model/ConnectedServices/Oauth/Microsoft/MicrosoftProvider.php - - - - message: '#^Method Model\\ConnectedServices\\Oauth\\Microsoft\\MicrosoftProvider\:\:getAuthorizationUrl\(\) throws checked exception InvalidArgumentException but it''s missing from the PHPDoc @throws tag\.$#' - identifier: missingType.checkedException - count: 1 - path: lib/Model/ConnectedServices/Oauth/Microsoft/MicrosoftProvider.php - - - - message: '#^Method Model\\ConnectedServices\\Oauth\\Microsoft\\MicrosoftProvider\:\:getResourceOwner\(\) throws checked exception TypeError but it''s missing from the PHPDoc @throws tag\.$#' - identifier: missingType.checkedException - count: 3 - path: lib/Model/ConnectedServices/Oauth/Microsoft/MicrosoftProvider.php - - - - message: '#^Method Model\\ConnectedServices\\Oauth\\Microsoft\\MicrosoftProvider\:\:getResourceOwner\(\) throws checked exception UnexpectedValueException but it''s missing from the PHPDoc @throws tag\.$#' - identifier: missingType.checkedException - count: 1 - path: lib/Model/ConnectedServices/Oauth/Microsoft/MicrosoftProvider.php - - - - message: '#^PHPDoc tag @return with type mixed is not subtype of native type Model\\ConnectedServices\\Oauth\\ProviderUser\.$#' - identifier: return.phpDocType - count: 1 - path: lib/Model/ConnectedServices/Oauth/Microsoft/MicrosoftProvider.php - - - - message: '#^Property Model\\ConnectedServices\\Oauth\\ProviderUser\:\:\$email \(string\) does not accept string\|null\.$#' - identifier: assign.propertyType - count: 1 - path: lib/Model/ConnectedServices/Oauth/Microsoft/MicrosoftProvider.php - - message: '#^Property Model\\ConnectedServices\\Oauth\\ProviderUser\:\:\$name \(string\) does not accept string\|null\.$#' identifier: assign.propertyType