mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(auth): accept Entra ID xms_edov claim as verified OIDC email
Microsoft Entra ID never sends email_verified, so existing accounts could not be linked through OIDC. Treat a boolean true xms_edov claim (email domain owner verified) as equivalent, via a shared OidcUser::claimsVerifyEmail() helper used for both ID token and userinfo claims. Document the Entra ID issuer URL and required optional claims in the OIDC settings helper text, and add tests covering xms_edov handling.
This commit is contained in:
@@ -25,8 +25,19 @@ class OidcUser extends SocialiteUser
|
||||
$this->idTokenClaims = $claims;
|
||||
$this->issuer = is_string($claims['iss'] ?? null) ? $claims['iss'] : null;
|
||||
$this->subject = is_string($claims['sub'] ?? null) ? $claims['sub'] : null;
|
||||
$this->emailVerified = ($claims['email_verified'] ?? false) === true;
|
||||
$this->emailVerified = self::claimsVerifyEmail($claims);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Microsoft Entra ID never sends email_verified. Its optional xms_edov
|
||||
* claim ("email domain owner verified") is the documented replacement.
|
||||
*
|
||||
* @param array<string, mixed> $claims
|
||||
*/
|
||||
public static function claimsVerifyEmail(array $claims): bool
|
||||
{
|
||||
return ($claims['email_verified'] ?? null) === true || ($claims['xms_edov'] ?? null) === true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ class OauthLoginService
|
||||
{
|
||||
[$issuer, $subject] = $this->oidcIssuerAndSubject($oauthUser);
|
||||
$emailVerified = ($oauthUser instanceof OidcUser && $oauthUser->emailVerified)
|
||||
|| data_get($oauthUser->user, 'email_verified') === true;
|
||||
|| OidcUser::claimsVerifyEmail(is_array($oauthUser->user ?? null) ? $oauthUser->user : []);
|
||||
|
||||
if (! is_string($issuer) || $issuer === '' || ! is_string($subject) || $subject === '') {
|
||||
throw new HttpException(403, 'OIDC provider did not return issuer and subject claims');
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
placeholder="{{ oauth_default_redirect_uri($provider) }}" label="Redirect URI" />
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.base_url" label="Issuer URL" required
|
||||
helper="OpenID Provider issuer URL, for example https://example.okta.com. Coolify uses it to discover the authorization, token, userinfo, and JWKS endpoints." />
|
||||
helper="OpenID Provider issuer URL, for example https://example.okta.com. Coolify uses it to discover the authorization, token, userinfo, and JWKS endpoints. For Microsoft Entra ID, use https://login.microsoftonline.com/<tenant ID>/v2.0 and add the optional ID token claims email and xms_edov to the app registration." />
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.client_id" label="Client ID" required />
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
|
||||
@@ -304,3 +304,37 @@ it('does not mark a newly provisioned oidc account verified without a verified e
|
||||
$user = User::whereEmail('unverified@example.com')->firstOrFail();
|
||||
expect($user->email_verified_at)->toBeNull();
|
||||
});
|
||||
|
||||
it('links an existing account when Microsoft Entra ID sends xms_edov instead of email_verified', function () {
|
||||
$user = User::factory()->create(['email' => 'member@example.com']);
|
||||
|
||||
fakeOidcProvider(['email' => 'member@example.com', 'email_verified' => null, 'xms_edov' => true]);
|
||||
|
||||
$this->get(route('auth.callback', 'oidc'))->assertRedirect('/');
|
||||
|
||||
$this->assertAuthenticatedAs($user);
|
||||
$this->assertDatabaseHas('oauth_identities', ['user_id' => $user->id, 'provider' => 'oidc']);
|
||||
});
|
||||
|
||||
it('rejects linking an existing account when Microsoft Entra ID sends a false xms_edov', function () {
|
||||
$user = User::factory()->create(['email' => 'member@example.com']);
|
||||
|
||||
fakeOidcProvider(['email' => 'member@example.com', 'email_verified' => null, 'xms_edov' => false]);
|
||||
|
||||
$this->from('/login')->get(route('auth.callback', 'oidc'))->assertRedirect('/login');
|
||||
|
||||
$this->assertGuest();
|
||||
$this->assertDatabaseMissing('oauth_identities', ['user_id' => $user->id]);
|
||||
});
|
||||
|
||||
it('treats only a boolean true email_verified or xms_edov id token claim as verified', function (array $claims, bool $expected) {
|
||||
$oidcUser = (new OidcUser)->setIdTokenClaims(['iss' => 'https://idp.example.com', 'sub' => 'user-1', ...$claims]);
|
||||
|
||||
expect($oidcUser->emailVerified)->toBe($expected);
|
||||
})->with([
|
||||
'email_verified true' => [['email_verified' => true], true],
|
||||
'xms_edov true' => [['xms_edov' => true], true],
|
||||
'xms_edov false' => [['xms_edov' => false], false],
|
||||
'xms_edov string' => [['xms_edov' => 'true'], false],
|
||||
'no claim' => [[], false],
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user