roundcube/roundcubemail · error · RuntimeException

Failed to validate JWT: missing aud/azp value

Error message

Failed to validate JWT: missing aud/azp value

What it means

Every OIDC token must bind to a party: jwt_decode() requires at least one of 'azp' or 'aud'. A token with neither cannot be attributed to this client, so it is rejected.

Solutions

  1. Configure the IDP to include 'aud' (or 'azp') in issued tokens.
  2. Verify the token type reaching jwt_decode is an ID/access/logout token, not a raw userinfo payload.
  3. Decode the token and confirm which claims exist; fix the issuing endpoint accordingly.
  4. If you control a test issuer, add "aud":"<client_id>" to the payload.

Example fix

// before
{"iss":"https://idp","sub":"u1","exp":1893456000}
// after
{"iss":"https://idp","sub":"u1","aud":"roundcube","exp":1893456000}
Defensive patterns

Strategy: validation

Validate before calling

$claims = json_decode(base64_decode(strtr(explode('.', $token)[1], '-_', '+/')), true);
if (!isset($claims['azp']) && !isset($claims['aud'])) { /* abort: unattributable token */ }

Type guard

function hasPartyClaim(array $claims): bool { return isset($claims['azp']) || isset($claims['aud']); }

Try / catch

try { $payload = $oauth->jwt_decode($token); } catch (\RuntimeException $e) { // reject token, redirect to login }

Prevention

When it happens

Trigger: parse_tokens() -> jwt_decode() on a token whose decoded payload contains neither 'azp' nor 'aud' claims.

Common situations: Custom or minimal IDP issuing bare JWTs without audience claims; malformed test tokens; provider emitting opaque-style tokens as JWTs without standard claims; userinfo-like payloads mistakenly passed through jwt_decode.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of roundcube/roundcubemail@4b54c2acfb (2026-09-14). Data as JSON: /api/errors/57f1ceda5d4e50a9. Report an issue: GitHub.

Appendix: source

Thrown at program/include/rcmail_oauth.php:452

            // Validate the token (throws exceptions)
            $header = new \stdClass();
            $body = (array) JWT::decode($jwt, JWK::parseKeySet($this->jwks), $header);
            $header = (array) $header;
        } else {
            [$headb64, $bodyb64, $cryptob64] = explode('.', $jwt);

            $header = json_decode(static::base64url_decode($headb64), true);
            $body = json_decode(static::base64url_decode($bodyb64), true);
            // $crypto = static::base64url_decode($cryptob64);
        }

        // FIXME depends on body type: ID, Logout, Bearer, Refresh,
        if (isset($body['azp']) && $body['azp'] !== $this->options['client_id']) {
            throw new \RuntimeException('Failed to validate JWT: invalid azp value');
        } elseif (isset($body['aud']) && !in_array($this->options['client_id'], (array) $body['aud'])) {
            throw new \RuntimeException('Failed to validate JWT: invalid aud value');
        } elseif (!isset($body['azp']) && !isset($body['aud'])) {
            throw new \RuntimeException('Failed to validate JWT: missing aud/azp value');
        }

        // if defined in parameters, check that issuer match
        if (isset($this->options['issuer']) && $body['iss'] !== $this->options['issuer']) {
            throw new \RuntimeException('Failed to validate JWT: issuer mismatch');
        }

        // check that token is not an outdated message
        if (isset($body['exp']) && (time() > $body['exp'])) {
            throw new \RuntimeException('Failed to validate JWT: expired message');
        }

        $body['header'] = $header;

        $this->log_debug('jwt: %s', json_encode($body));

        return $body;
    }

View on GitHub (pinned to 4b54c2acfb)