-
Notifications
You must be signed in to change notification settings - Fork 274
π fix: migrate Microsoft OAuth to v2.0 endpoints and Graph API #4551
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
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,21 +1,25 @@ | ||||||||||||||||||||||
| <?php | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| 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 | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| 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 @@ | |||||||||||||||||||||
| '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 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); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
Comment on lines
+82
to
+83
|
||||||||||||||||||||||
| $data = json_decode($response->getBody()->getContents(), true); | |
| try { | |
| $data = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); | |
| } catch (\JsonException $e) { | |
| throw new \RuntimeException('Invalid JSON received from Microsoft token endpoint.', 0, $e); | |
| } | |
| if (!is_array($data) || !isset($data['access_token']) || !is_string($data['access_token']) || $data['access_token'] === '') { | |
| throw new \RuntimeException('Microsoft token endpoint returned an invalid access token payload.'); | |
| } |
Copilot
AI
Apr 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ProviderUser::$email is declared as string, but this assignment can produce null (e.g., when both mail and userPrincipalName are missing). That will trigger a TypeError at runtime. Ensure a non-null string is assigned (or change the model to allow ?string and handle it consistently across providers).
| $user->email = $data['mail'] ?? $data['userPrincipalName'] ?? null; | |
| $user->email = $data['mail'] ?? $data['userPrincipalName'] ?? ''; |
Copilot
AI
Apr 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ProviderUser::$name is declared as string, but $data['givenName'] ?? null can evaluate to null, which will cause a TypeError when assigned. Please provide a non-null fallback (or update ProviderUser typing and downstream consumers accordingly).
| $user->name = $data['givenName'] ?? null; | |
| $user->lastName = $data['surname'] ?? null; | |
| $user->name = $data['givenName'] ?? ''; | |
| $user->lastName = $data['surname'] ?? ''; |
Copilot
AI
Apr 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the token exchange, the Graph /me response is decoded with json_decode(..., true) without checking for decode errors or expected shape. If the response body isn't valid JSON or isn't an array, the following field reads will yield null and can cascade into TypeErrors when populating ProviderUser. Consider using JSON_THROW_ON_ERROR and validating the decoded payload before mapping fields.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Comment on lines
8712
to
8717
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both methods instantiate a new
GuzzleHttp\Clientwith default options. Without explicittimeout/connect_timeout, a slow or stalled external request can hang the PHP worker. Consider configuring sensible timeouts (and optionally reusing a single client instance) to reduce operational risk.