Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 6 additions & 33 deletions src/webauthn/src/CeremonyStep/CheckAllowedOrigins.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,12 @@
final readonly class CheckAllowedOrigins implements CeremonyStep
{
/**
* Full origin entries (scheme://host[:port]) from allowed origins that include a scheme.
* Full origin entries (scheme://host[:port]) from allowed origins.
*
* @var string[]
*/
private array $fullOrigins;

/**
* Host-only entries from allowed origins without a scheme (backward compatibility).
*
* @var string[]
*/
private array $hostOrigins;

/**
* @param string[] $allowedOrigins
* @param string[] $securedRelyingPartyId RP IDs that are allowed to use HTTP (e.g. localhost for development)
Expand All @@ -46,19 +39,19 @@ public function __construct(
private array $securedRelyingPartyId = [],
) {
$fullOrigins = [];
$hostOrigins = [];
foreach ($allowedOrigins as $allowedOrigin) {
$parsed = parse_url($allowedOrigin);
$parsed !== false || throw new InvalidArgumentException(sprintf('Invalid origin: %s', $allowedOrigin));
if (isset($parsed['scheme'], $parsed['host'])) {
$fullOrigins[] = self::buildOrigin($parsed['scheme'], $parsed['host'], $parsed['port'] ?? null);
} else {
$hostOrigins[] = $parsed['host'] ?? $allowedOrigin;
// Host-only entries are normalized to https:// since WebAuthn requires TLS
$host = $parsed['host'] ?? $allowedOrigin;
$fullOrigins[] = self::buildOrigin('https', $host, null);
}
}

$this->fullOrigins = array_unique($fullOrigins);
$this->hostOrigins = array_unique($hostOrigins);
}

public function process(
Expand All @@ -77,7 +70,7 @@ public function process(
);
$originHost = $parsedOrigin['host'] ?? $C->origin;

$hasAllowedOrigins = count($this->fullOrigins) !== 0 || count($this->hostOrigins) !== 0;
$hasAllowedOrigins = count($this->fullOrigins) !== 0;

if ($hasAllowedOrigins) {
// Full origin match (scheme + host + port)
Expand All @@ -92,15 +85,8 @@ public function process(
}
}

// Host-only match (backward compatibility for entries without scheme)
if (in_array($originHost, $this->hostOrigins, true)) {
return;
}

// Subdomain matching
$isFullOriginSubdomain = $this->isSubdomainOfFullOrigins($parsedOrigin);
$isHostSubdomain = $this->isSubdomain($this->hostOrigins, $originHost);
$isSubDomain = $isFullOriginSubdomain || $isHostSubdomain;
$isSubDomain = $this->isSubdomainOfFullOrigins($parsedOrigin);

if ($this->allowSubdomains && $isSubDomain) {
return;
Expand Down Expand Up @@ -214,17 +200,4 @@ private function getFacetId(

return (is_string($appId) && $wasUsed === true) ? $appId : $rpId;
}

/**
* @param string[] $origins
*/
private function isSubdomain(array $origins, string $domain): bool
{
foreach ($origins as $allowedOrigin) {
if ($this->isSubdomainOf($domain, $allowedOrigin)) {
return true;
}
}
return false;
}
}
Loading