email = 'alice@example.com'; $c->email_verified = true; self::assertSame( 'alice@example.com', OidcClaims::resolveEmail($c, self::OID) ); } public function testTrustsEmailWhenVerifiedFlagIsAbsent(): void { // Entra v2.0 work/school accounts do not emit email_verified; // the email comes from the directory and is server-controlled. $c = new stdClass(); $c->email = 'alice@example.com'; self::assertSame( 'alice@example.com', OidcClaims::resolveEmail($c, self::OID) ); } public function testFallsBackWhenIssuerExplicitlyUnverified(): void { $c = new stdClass(); $c->email = 'spoofed@example.com'; $c->email_verified = false; self::assertSame( 'entra:' . self::OID, OidcClaims::resolveEmail($c, self::OID) ); } public function testNeverFallsBackToPreferredUsername(): void { // The actual hazard the finding flagged: preferred_username may // be user-controlled. Even when email is missing entirely, the // resolver must NOT pick it up. $c = new stdClass(); $c->preferred_username = 'attacker@victim.example'; self::assertSame( 'entra:' . self::OID, OidcClaims::resolveEmail($c, self::OID) ); } public function testEmptyEmailFallsBack(): void { $c = new stdClass(); $c->email = ''; $c->email_verified = true; self::assertSame( 'entra:' . self::OID, OidcClaims::resolveEmail($c, self::OID) ); } public function testWhitespaceOnlyEmailFallsBack(): void { $c = new stdClass(); $c->email = " \t "; $c->email_verified = true; self::assertSame( 'entra:' . self::OID, OidcClaims::resolveEmail($c, self::OID) ); } public function testEmailIsTrimmed(): void { $c = new stdClass(); $c->email = ' alice@example.com '; self::assertSame( 'alice@example.com', OidcClaims::resolveEmail($c, self::OID) ); } public function testNoClaimsAtAllFallsBack(): void { $c = new stdClass(); self::assertSame( 'entra:' . self::OID, OidcClaims::resolveEmail($c, self::OID) ); } public function testTruthyButNonBooleanVerifiedStillTrusted(): void { // Per the OIDC core spec email_verified should be boolean. Some // IdPs ship strings; we only treat literal `false` as a negative // signal — anything else is "no negative signal" and we trust // the email. $c = new stdClass(); $c->email = 'alice@example.com'; $c->email_verified = 'false'; // string, not bool false self::assertSame( 'alice@example.com', OidcClaims::resolveEmail($c, self::OID) ); } public function testNonScalarEmailTreatedAsMissing(): void { // Defence against unexpected JWT decoder shapes (e.g. nested // objects, arrays). is_scalar() guards the cast. $c = new stdClass(); $c->email = ['alice@example.com']; self::assertSame( 'entra:' . self::OID, OidcClaims::resolveEmail($c, self::OID) ); } }