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

  1. Set passbolt App.fullBaseUrl to the exact public URL clients use (scheme + host + port, no trailing slash issues)
  2. Make the client build the challenge domain from the same base URL it uses to reach the API
  3. Fix reverse proxy headers (X-Forwarded-Proto/Host) or disable auto scheme detection so Router::url full matches reality
  4. Read the 'Expected: X and got Y' message in the response/logs and align the client to X
  5. 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

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


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)