BookStackApp/BookStack · error · OidcInvalidTokenException

Token audience value has ' . count($aud) . ' values, Expecte

Error message

Token audience value has ' . count($aud) . ' values, Expected 1

What it means

OidcIdToken::validateTokenClaims enforces the OpenID Connect rule that the ID token 'aud' claim must contain exactly one audience — BookStack's client ID. The parent JWT validation only partially checks audiences, so this code rejects tokens whose aud is an array with 0, 2+ values, or otherwise not a single value.

Source

Thrown at app/Access/Oidc/OidcIdToken.php:39

     * Validate the claims of the token.
     * As per https://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation.
     *
     * @throws OidcInvalidTokenException
     */
    protected function validateTokenClaims(string $clientId): void
    {
        // 1. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery)
        // MUST exactly match the value of the iss (issuer) Claim.
        // Already done in parent.

        // 2. The Client MUST validate that the aud (audience) Claim contains its client_id value registered
        // at the Issuer identified by the iss (issuer) Claim as an audience. The ID Token MUST be rejected
        // if the ID Token does not list the Client as a valid audience, or if it contains additional
        // audiences not trusted by the Client.
        // Partially done in parent.
        $aud = is_string($this->payload['aud']) ? [$this->payload['aud']] : $this->payload['aud'];
        if (count($aud) !== 1) {
            throw new OidcInvalidTokenException('Token audience value has ' . count($aud) . ' values, Expected 1');
        }

        // 3. If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present.
        // NOTE: Addressed by enforcing a count of 1 above.

        // 4. If an azp (authorized party) Claim is present, the Client SHOULD verify that its client_id
        // is the Claim Value.
        if (isset($this->payload['azp']) && $this->payload['azp'] !== $clientId) {
            throw new OidcInvalidTokenException('Token authorized party exists but does not match the expected client_id');
        }

        // 5. The current time MUST be before the time represented by the exp Claim
        // (possibly allowing for some small leeway to account for clock skew).
        if (empty($this->payload['exp'])) {
            throw new OidcInvalidTokenException('Missing token expiration time value');
        }

        $skewSeconds = 120;

View on GitHub (pinned to 18f8469a1c)

Solutions

  1. Configure the IdP to issue tokens with a single audience equal to BookStack's client ID
  2. Check the raw ID token (jwt.io decode) to see what aud values the IdP is sending
  3. If the IdP adds extra audiences, remove the additional audiences/client scopes or use a dedicated client for BookStack
  4. Verify the OIDC client_id in BookStack matches the intended audience configured at the IdP
  5. If the IdP requires multi-audience tokens, check for an azp claim setup per OIDC spec or use a proxy mapper to force single aud

Example fix

// before (IdP token)
"aud": ["bookstack-client", "other-api-client"]
// after (IdP client config)
"aud": "bookstack-client"
Defensive patterns

Strategy: validation

Validate before calling

$payload = json_decode(base64_decode(str_replace('_', '/', str_replace('-', '+', explode('.', $idToken)[1]))), true);
$aud = $payload['aud'] ?? null;
if (is_array($aud) ? count($aud) !== 1 : empty($aud)) {
    throw new InvalidArgumentException('ID token must have exactly one audience');
}

Type guard

function hasSingleAudience(?array $payload): bool {
    $aud = $payload['aud'] ?? null;
    return is_string($aud) || (is_array($aud) && count($aud) === 1);
}

Try / catch

try {
    $idToken = OidcIdToken::validate($token, $clientId, $keys);
} catch (OidcInvalidTokenException $e) {
    Log::error('OIDC token rejected', ['reason' => $e->getMessage()]);
    return redirect('/login')->withErrors('OIDC provider returned an invalid token');
}

Prevention

When it happens

Trigger: validate() on an OIDC ID token whose payload['aud'] is a multi-value array (count != 1) or an empty/absent audience, during OIDC login token processing.

Common situations: IdP configured with multiple audiences for the client (e.g. also issuing tokens for an API); IdP sending aud as array by default; misconfigured client where BookStack's client ID is added as an additional audience alongside a resource server audience; auto-discovery pointing at a different client's realm.

Related errors


AI-assisted analysis of BookStackApp/BookStack@18f8469a1c (2026-09-02). Data as JSON: /api/errors/1d12b84be639ae89. Report an issue: GitHub.