|
|
@@ -0,0 +1,144 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+declare(strict_types=1);
|
|
|
+
|
|
|
+namespace App\Tests\Auth;
|
|
|
+
|
|
|
+use App\Auth\OidcClaims;
|
|
|
+use App\Tests\TestCase;
|
|
|
+use stdClass;
|
|
|
+
|
|
|
+/**
|
|
|
+ * R01-N18 — pin the email-resolution rules. The function decides what
|
|
|
+ * string ends up in `users.email` and `audit_log.user_email` for OIDC
|
|
|
+ * users. Identity is keyed by `oid`, which is unaffected.
|
|
|
+ */
|
|
|
+final class OidcClaimsTest extends TestCase
|
|
|
+{
|
|
|
+ private const OID = '00000000-0000-0000-0000-000000000abc';
|
|
|
+
|
|
|
+ public function testTrustsEmailWhenExplicitlyVerified(): void
|
|
|
+ {
|
|
|
+ $c = new stdClass();
|
|
|
+ $c->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)
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|