OidcClaimsTest.php 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Auth;
  4. use App\Auth\OidcClaims;
  5. use App\Tests\TestCase;
  6. use stdClass;
  7. /**
  8. * R01-N18 — pin the email-resolution rules. The function decides what
  9. * string ends up in `users.email` and `audit_log.user_email` for OIDC
  10. * users. Identity is keyed by `oid`, which is unaffected.
  11. */
  12. final class OidcClaimsTest extends TestCase
  13. {
  14. private const OID = '00000000-0000-0000-0000-000000000abc';
  15. public function testTrustsEmailWhenExplicitlyVerified(): void
  16. {
  17. $c = new stdClass();
  18. $c->email = 'alice@example.com';
  19. $c->email_verified = true;
  20. self::assertSame(
  21. 'alice@example.com',
  22. OidcClaims::resolveEmail($c, self::OID)
  23. );
  24. }
  25. public function testTrustsEmailWhenVerifiedFlagIsAbsent(): void
  26. {
  27. // Entra v2.0 work/school accounts do not emit email_verified;
  28. // the email comes from the directory and is server-controlled.
  29. $c = new stdClass();
  30. $c->email = 'alice@example.com';
  31. self::assertSame(
  32. 'alice@example.com',
  33. OidcClaims::resolveEmail($c, self::OID)
  34. );
  35. }
  36. public function testFallsBackWhenIssuerExplicitlyUnverified(): void
  37. {
  38. $c = new stdClass();
  39. $c->email = 'spoofed@example.com';
  40. $c->email_verified = false;
  41. self::assertSame(
  42. 'entra:' . self::OID,
  43. OidcClaims::resolveEmail($c, self::OID)
  44. );
  45. }
  46. public function testNeverFallsBackToPreferredUsername(): void
  47. {
  48. // The actual hazard the finding flagged: preferred_username may
  49. // be user-controlled. Even when email is missing entirely, the
  50. // resolver must NOT pick it up.
  51. $c = new stdClass();
  52. $c->preferred_username = 'attacker@victim.example';
  53. self::assertSame(
  54. 'entra:' . self::OID,
  55. OidcClaims::resolveEmail($c, self::OID)
  56. );
  57. }
  58. public function testEmptyEmailFallsBack(): void
  59. {
  60. $c = new stdClass();
  61. $c->email = '';
  62. $c->email_verified = true;
  63. self::assertSame(
  64. 'entra:' . self::OID,
  65. OidcClaims::resolveEmail($c, self::OID)
  66. );
  67. }
  68. public function testWhitespaceOnlyEmailFallsBack(): void
  69. {
  70. $c = new stdClass();
  71. $c->email = " \t ";
  72. $c->email_verified = true;
  73. self::assertSame(
  74. 'entra:' . self::OID,
  75. OidcClaims::resolveEmail($c, self::OID)
  76. );
  77. }
  78. public function testEmailIsTrimmed(): void
  79. {
  80. $c = new stdClass();
  81. $c->email = ' alice@example.com ';
  82. self::assertSame(
  83. 'alice@example.com',
  84. OidcClaims::resolveEmail($c, self::OID)
  85. );
  86. }
  87. public function testNoClaimsAtAllFallsBack(): void
  88. {
  89. $c = new stdClass();
  90. self::assertSame(
  91. 'entra:' . self::OID,
  92. OidcClaims::resolveEmail($c, self::OID)
  93. );
  94. }
  95. public function testTruthyButNonBooleanVerifiedStillTrusted(): void
  96. {
  97. // Per the OIDC core spec email_verified should be boolean. Some
  98. // IdPs ship strings; we only treat literal `false` as a negative
  99. // signal — anything else is "no negative signal" and we trust
  100. // the email.
  101. $c = new stdClass();
  102. $c->email = 'alice@example.com';
  103. $c->email_verified = 'false'; // string, not bool false
  104. self::assertSame(
  105. 'alice@example.com',
  106. OidcClaims::resolveEmail($c, self::OID)
  107. );
  108. }
  109. public function testNonScalarEmailTreatedAsMissing(): void
  110. {
  111. // Defence against unexpected JWT decoder shapes (e.g. nested
  112. // objects, arrays). is_scalar() guards the cast.
  113. $c = new stdClass();
  114. $c->email = ['alice@example.com'];
  115. self::assertSame(
  116. 'entra:' . self::OID,
  117. OidcClaims::resolveEmail($c, self::OID)
  118. );
  119. }
  120. }