ellite/Wallos · error · InvalidArgumentException

Not a valid OTP provisioning URI

Error message

Not a valid OTP provisioning URI

What it means

After parsing the otpauth URI's query string, Url::fromString() requires a 'secret' key. Line 94 throws 'Not a valid OTP provisioning URI' when the query has no secret parameter, since a provisioning URI without a shared secret cannot produce a working OTP.

Solutions

  1. Append the secret to the query string: ?secret=BASE32SECRET
  2. Verify the parameter name is exactly 'secret' (lowercase)
  3. If you lack a secret, create the OTP from a generated one via TOTP::generateSecret()

Example fix

// before
$otp->loadProvisioningUri('otpauth://totp/alice?digits=6');
// after
$otp->loadProvisioningUri('otpauth://totp/alice?secret=JBSWY3DPEHPK3PXP&digits=6');
Defensive patterns

Strategy: validation

Validate before calling

$parts = parse_url($uri);
parse_str($parts['query'] ?? '', $q);
if (!isset($q['secret']) || $q['secret'] === '') {
    throw new \LogicException('URI must contain a secret query parameter');
}

Try / catch

try {
    $otp->loadProvisioningUri($uri);
} catch (\InvalidArgumentException $e) {
    // prompt user to re-enroll / re-scan QR code
}

Prevention

When it happens

Trigger: Calling loadProvisioningUri() with 'otpauth://totp/alice?digits=6&period=30' — scheme/host/path are fine, but parse_str yields no 'secret' key, or the secret query key is misspelled (e.g. 'secrets').

Common situations: Sharing URIs where the secret was stripped for security; hand-writing URIs and forgetting secret=; URI builders that URL-encode the secret under a different parameter name.

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 ellite/Wallos@52820e87ca (2026-09-13). Data as JSON: /api/errors/f3c11691955514a9. Report an issue: GitHub.

Appendix: source

Thrown at libs/OTPHP/Url.php:94

    {
        $parsed_url = parse_url($uri);
        $parsed_url !== false || throw new InvalidArgumentException('Invalid URI.');
        foreach (['scheme', 'host', 'path', 'query'] as $key) {
            array_key_exists($key, $parsed_url) || throw new InvalidArgumentException(
                'Not a valid OTP provisioning URI'
            );
        }
        $scheme = $parsed_url['scheme'] ?? null;
        $host = $parsed_url['host'] ?? null;
        $path = $parsed_url['path'] ?? null;
        $query = $parsed_url['query'] ?? null;
        $scheme === 'otpauth' || throw new InvalidArgumentException('Not a valid OTP provisioning URI');
        is_string($host) || throw new InvalidArgumentException('Invalid URI.');
        is_string($path) || throw new InvalidArgumentException('Invalid URI.');
        is_string($query) || throw new InvalidArgumentException('Invalid URI.');
        $parsedQuery = [];
        parse_str($query, $parsedQuery);
        array_key_exists('secret', $parsedQuery) || throw new InvalidArgumentException(
            'Not a valid OTP provisioning URI'
        );
        $secret = $parsedQuery['secret'];
        unset($parsedQuery['secret']);

        return new self($scheme, $host, $path, $secret, $parsedQuery);
    }
}

View on GitHub (pinned to 52820e87ca)