{"record":{"id":"0262df50dc1ab0ba","repo":"BookStackApp/BookStack","slug":"no-valid-subject-value-found-in-userinfo-data","errorCode":null,"errorMessage":"No valid subject value found in userinfo data","messagePattern":"No valid subject value found in userinfo data","errorType":"exception","errorClass":"OidcInvalidTokenException","httpStatus":null,"severity":"error","filePath":"app/Access/Oidc/OidcUserinfoResponse.php","lineNumber":40,"sourceCode":"            $this->jwt = new OidcJwtWithClaims($response->getBody()->getContents(), $issuer, $keys);\n            $this->claims = $this->jwt->getAllClaims();\n        }\n    }\n\n    /**\n     * @throws OidcInvalidTokenException\n     */\n    public function validate(string $idTokenSub, string $clientId): bool\n    {\n        if (!is_null($this->jwt)) {\n            $this->jwt->validateCommonTokenDetails($clientId);\n        }\n\n        $sub = $this->getClaim('sub');\n\n        // Spec: v1.0 5.3.2: The sub (subject) Claim MUST always be returned in the UserInfo Response.\n        if (!is_string($sub) || empty($sub)) {\n            throw new OidcInvalidTokenException(\"No valid subject value found in userinfo data\");\n        }\n\n        // Spec: v1.0 5.3.2: The sub Claim in the UserInfo Response MUST be verified to exactly match the sub Claim in the ID Token;\n        // if they do not match, the UserInfo Response values MUST NOT be used.\n        if ($idTokenSub !== $sub) {\n            throw new OidcInvalidTokenException(\"Subject value provided in the userinfo endpoint does not match the provided ID token value\");\n        }\n\n        // Spec v1.0 5.3.4 Defines the following:\n        // Verify that the OP that responded was the intended OP through a TLS server certificate check, per RFC 6125 [RFC6125].\n          // This is effectively done as part of the HTTP request we're making through CURLOPT_SSL_VERIFYHOST on the request.\n        // If the Client has provided a userinfo_encrypted_response_alg parameter during Registration, decrypt the UserInfo Response using the keys specified during Registration.\n          // We don't currently support JWT encryption for OIDC\n        // If the response was signed, the Client SHOULD validate the signature according to JWS [JWS].\n          // This is done as part of the validateCommonClaims above.\n\n        return true;\n    }","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/BookStackApp/BookStack/blob/18f8469a1c72f8cc8497e9372635e6dea5028071/app/Access/Oidc/OidcUserinfoResponse.php#L22-L58","documentation":"Per OpenID Connect spec v1.0 §5.3.2, the UserInfo response MUST include a non-empty string 'sub' claim. OidcUserinfoResponse::validate() throws this OidcInvalidTokenException when the 'sub' claim is absent, empty, or not a string, because the response cannot be trusted or correlated with the ID token.","triggerScenarios":"validate() is called from getUserDetailsFromToken; $this->getClaim('sub') returns null, an empty string, or a non-string (number/object), so the is_string/empty guard fails.","commonSituations":"IdP violating the OIDC spec (omitting sub in userinfo), a broken reverse proxy mangling the JSON, custom/misconfigured userinfo endpoint URL in .env pointing at the wrong route, or an IdP returning an error payload that still parses as JSON.","solutions":["Verify OIDC_USERINFO_ENDPOINT points at the correct userinfo URL of the IdP","Inspect the raw userinfo response (curl with the access token) and check the 'sub' claim","Update or patch the IdP to include a string 'sub' claim per OIDC Core §5.3.2","If the IdP cannot be fixed, disable the userinfo endpoint so claims are read from the ID token"],"exampleFix":"// before (wrong endpoint, returns token introspection JSON without sub)\nOIDC_USERINFO_ENDPOINT=https://idp.example.com/introspect\n// after\nOIDC_USERINFO_ENDPOINT=https://idp.example.com/userinfo","handlingStrategy":"validation","validationCode":"// Validate the userinfo payload manually before login flows:\n$ui = json_decode($rawUserinfoBody, true);\nif (!is_array($ui) || !isset($ui['sub']) || !is_string($ui['sub']) || $ui['sub'] === '') {\n    throw new DomainException('IdP userinfo response lacks a valid string sub claim');\n}","typeGuard":null,"tryCatchPattern":"try {\n    auth()->attemptOidcLogin();\n} catch (BookStack\\Access\\Oidc\\OidcException $e) {\n    if (str_contains($e->getMessage(), 'Userinfo endpoint')) {\n        Log::error('IdP userinfo invalid', ['detail' => $e->getMessage()]);\n        abort(502, 'Identity provider userinfo response is not OIDC compliant');\n    }\n    throw $e;\n}","preventionTips":["Verify the IdP follows OIDC Core §5.3.2 (sub always present)","Point OIDC_USERINFO_ENDPOINT at the real userinfo URL","Curl the userinfo endpoint with a real access token during setup","Update IdP versions with known userinfo compliance bugs"],"tags":["oidc","userinfo","spec-compliance"],"backgroundTag":"missing-sub-claim","analyzedSha":"18f8469a1c72f8cc8497e9372635e6dea5028071","analyzedAt":"2026-09-02T19:49:33.068Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}