| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152 |
- <?php
- /*
- * Copyright 2026 Alessandro Chiapparini <sprint_planer_web@chiapparini.org>
- * SPDX-License-Identifier: Apache-2.0
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * See the LICENSE file in the project root for the full license text.
- */
- 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)
- );
- }
- }
|