passbolt/passbolt_api · error · Passbolt\JwtAuthentication\Error\Exception\Challenge\InvalidDomainException
The domain is invalid.
Error message
The domain is invalid.
What it means
assertDomain verifies the challenge's domain field matches the server's own full base URL (Router::url('/', true)) modulo trailing slashes. InvalidDomainException is thrown (and may trigger email alerts) when the domain is missing, non-string, or points to a different host — a common anti-phishing check in the GPG auth protocol.
Solutions
- Set passbolt App.fullBaseUrl to the exact public URL clients use (scheme + host + port, no trailing slash issues)
- Make the client build the challenge domain from the same base URL it uses to reach the API
- Fix reverse proxy headers (X-Forwarded-Proto/Host) or disable auto scheme detection so Router::url full matches reality
- Read the 'Expected: X and got Y' message in the response/logs and align the client to X
- Avoid accessing the server through aliases (IP, internal hostname) that differ from fullBaseUrl
Example fix
// before config/app.php 'fullBaseUrl' => 'http://localhost', // after 'fullBaseUrl' => 'https://passbolt.example.com'; // must equal the domain field clients put in the challenge
Defensive patterns
Strategy: validation
Validate before calling
const apiBase = new URL(apiUrl).origin + '/'; if (rtrim(challenge.domain) !== rtrim(apiBase)) challenge.domain = apiBase;
Type guard
function domainMatches(c, base) { return typeof c.domain === 'string' && c.domain.replace(/\/+$/, '') === base.replace(/\/+$/, ''); } Try / catch
try { await login(challenge); } catch (e) { if (/domain is invalid. Expected/.test(e.message)) { const expected = parseExpected(e.message); await login({ ...challenge, domain: expected }); } } Prevention
- Derive the challenge domain from the same base URL used for API calls
- Set App.fullBaseUrl correctly (scheme/host/port) before client rollouts
- Fix proxy headers so the server sees the public URL
- Strip trailing slashes consistently on both sides
- Avoid mixing http/https or IP/hostname between config and clients
When it happens
Trigger: POST /auth/jwt/login where the challenge's domain is absent/not a string, or differs from the configured App.fullBaseUrl: client used http vs https, localhost vs production host, a different port, or a trailing-path mismatch.
Common situations: App.fullBaseUrl misconfigured (defaulting to localhost) while clients reach the server via a real domain; reverse proxy stripping/altering the scheme; client SDK auto-detecting the API URL from a different base; accessing via IP while fullBaseUrl is the hostname.
Understand the failure class
Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.
Related errors
- The challenge cannot be decrypted.
- The challenge is invalid. Deserialization failed.
- The challenge is invalid. Validation Failed.
- The configuration is not correctly set.
- The JWT public key could not be extracted.
AI-assisted analysis of passbolt/passbolt_api@31c1bbc10f (2026-09-17).
Data as JSON: /api/errors/9c883462760de12a.
Report an issue: GitHub.
Appendix: source
Thrown at plugins/PassboltCe/JwtAuthentication/src/Authenticator/GpgJwtAuthenticator.php:444
*/
public function assertVersion(mixed $version): void
{
if (!isset($version) || !is_string($version) || $version !== self::PROTOCOL_VERSION) {
throw new Exception(__('The version is invalid.'));
}
}
/**
* Assert domain
*
* @param mixed $domain domain
* @return void
* @throws \Passbolt\JwtAuthentication\Error\Exception\Challenge\InvalidDomainException if domain is invalid
*/
public function assertDomain(mixed $domain): void
{
if (!isset($domain) || !is_string($domain)) {
throw new InvalidDomainException(__('The domain is invalid.'));
}
if (rtrim($domain, '/') !== rtrim(Router::url('/', true), '/')) {
$expect = rtrim(Router::url('/', true));
$got = rtrim($domain, '/');
throw new InvalidDomainException(__('The domain is invalid. Expected: {0} and got {1}', $expect, $got));
}
}
/**
* @return \App\Utility\OpenPGP\OpenPGPBackend
*/
public function getGpg(): OpenPGPBackend
{
return $this->gpg;
}
/**View on GitHub (pinned to 31c1bbc10f)