{"record":{"id":"1d12b84be639ae89","repo":"BookStackApp/BookStack","slug":"token-audience-value-has-count-aud-value","errorCode":null,"errorMessage":"Token audience value has ' . count($aud) . ' values, Expected 1","messagePattern":"Token audience value has ' \\. count\\(\\$aud\\) \\. ' values, Expected 1","errorType":"exception","errorClass":"OidcInvalidTokenException","httpStatus":null,"severity":"error","filePath":"app/Access/Oidc/OidcIdToken.php","lineNumber":39,"sourceCode":"     * Validate the claims of the token.\n     * As per https://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation.\n     *\n     * @throws OidcInvalidTokenException\n     */\n    protected function validateTokenClaims(string $clientId): void\n    {\n        // 1. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery)\n        // MUST exactly match the value of the iss (issuer) Claim.\n        // Already done in parent.\n\n        // 2. The Client MUST validate that the aud (audience) Claim contains its client_id value registered\n        // at the Issuer identified by the iss (issuer) Claim as an audience. The ID Token MUST be rejected\n        // if the ID Token does not list the Client as a valid audience, or if it contains additional\n        // audiences not trusted by the Client.\n        // Partially done in parent.\n        $aud = is_string($this->payload['aud']) ? [$this->payload['aud']] : $this->payload['aud'];\n        if (count($aud) !== 1) {\n            throw new OidcInvalidTokenException('Token audience value has ' . count($aud) . ' values, Expected 1');\n        }\n\n        // 3. If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present.\n        // NOTE: Addressed by enforcing a count of 1 above.\n\n        // 4. If an azp (authorized party) Claim is present, the Client SHOULD verify that its client_id\n        // is the Claim Value.\n        if (isset($this->payload['azp']) && $this->payload['azp'] !== $clientId) {\n            throw new OidcInvalidTokenException('Token authorized party exists but does not match the expected client_id');\n        }\n\n        // 5. The current time MUST be before the time represented by the exp Claim\n        // (possibly allowing for some small leeway to account for clock skew).\n        if (empty($this->payload['exp'])) {\n            throw new OidcInvalidTokenException('Missing token expiration time value');\n        }\n\n        $skewSeconds = 120;","sourceCodeStart":21,"sourceCodeEnd":57,"githubUrl":"https://github.com/BookStackApp/BookStack/blob/18f8469a1c72f8cc8497e9372635e6dea5028071/app/Access/Oidc/OidcIdToken.php#L21-L57","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Configure the IdP to issue tokens with a single audience equal to BookStack's client ID","Check the raw ID token (jwt.io decode) to see what aud values the IdP is sending","If the IdP adds extra audiences, remove the additional audiences/client scopes or use a dedicated client for BookStack","Verify the OIDC client_id in BookStack matches the intended audience configured at the IdP","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"],"exampleFix":"// before (IdP token)\n\"aud\": [\"bookstack-client\", \"other-api-client\"]\n// after (IdP client config)\n\"aud\": \"bookstack-client\"","handlingStrategy":"validation","validationCode":"$payload = json_decode(base64_decode(str_replace('_', '/', str_replace('-', '+', explode('.', $idToken)[1]))), true);\n$aud = $payload['aud'] ?? null;\nif (is_array($aud) ? count($aud) !== 1 : empty($aud)) {\n    throw new InvalidArgumentException('ID token must have exactly one audience');\n}","typeGuard":"function hasSingleAudience(?array $payload): bool {\n    $aud = $payload['aud'] ?? null;\n    return is_string($aud) || (is_array($aud) && count($aud) === 1);\n}","tryCatchPattern":"try {\n    $idToken = OidcIdToken::validate($token, $clientId, $keys);\n} catch (OidcInvalidTokenException $e) {\n    Log::error('OIDC token rejected', ['reason' => $e->getMessage()]);\n    return redirect('/login')->withErrors('OIDC provider returned an invalid token');\n}","preventionTips":["Configure the IdP client so tokens have exactly one aud equal to BookStack's client_id","Avoid sharing one client across BookStack and API resources","Decode a sample token at jwt.io when setting up OIDC","Keep auto-discovery issuer and client_id consistent"],"tags":["oidc","jwt","openid-connect","token-validation"],"backgroundTag":"jwt-audience-mismatch","analyzedSha":"18f8469a1c72f8cc8497e9372635e6dea5028071","analyzedAt":"2026-09-02T19:49:33.068Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}