{"record":{"id":"8cb6b2dbbf50b37b","repo":"spring-projects/spring-security","slug":"invalid-request-8cb6b2","errorCode":"invalid_request","errorMessage":"invalid_request","messagePattern":"invalid_request","errorType":"error_code","errorClass":"OAuth2AuthenticationException","httpStatus":400,"severity":"error","filePath":"oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/PublicClientAuthenticationConverter.java","lineNumber":65,"sourceCode":" * @see <a target=\"_blank\" href=\"https://tools.ietf.org/html/rfc7636\">Proof Key for Code\n * Exchange by OAuth Public Clients</a>\n */\npublic final class PublicClientAuthenticationConverter implements AuthenticationConverter {\n\n\t@Override\n\tpublic @Nullable Authentication convert(HttpServletRequest request) {\n\t\tif (!OAuth2EndpointUtils.matchesPkceTokenRequest(request)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tMultiValueMap<String, String> parameters = \"GET\".equals(request.getMethod())\n\t\t\t\t? OAuth2EndpointUtils.getQueryParameters(request) : OAuth2EndpointUtils.getFormParameters(request);\n\n\t\t// client_id (REQUIRED for public clients)\n\t\tString clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);\n\t\tList<String> clientIdParams = parameters.get(OAuth2ParameterNames.CLIENT_ID);\n\t\tif (!StringUtils.hasText(clientId) || clientIdParams == null || clientIdParams.size() != 1) {\n\t\t\tthrow new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);\n\t\t}\n\n\t\t// code_verifier (REQUIRED)\n\t\tList<String> codeVerifierParams = parameters.get(PkceParameterNames.CODE_VERIFIER);\n\t\tif (codeVerifierParams == null || codeVerifierParams.size() != 1) {\n\t\t\tthrow new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);\n\t\t}\n\n\t\tparameters.remove(OAuth2ParameterNames.CLIENT_ID);\n\n\t\tMap<String, Object> additionalParameters = new HashMap<>();\n\t\tparameters.forEach((key, value) -> additionalParameters.put(key,\n\t\t\t\t(value.size() == 1) ? value.get(0) : value.toArray(new String[0])));\n\n\t\treturn new OAuth2ClientAuthenticationToken(clientId, ClientAuthenticationMethod.NONE, null,\n\t\t\t\tadditionalParameters);\n\t}\n","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/spring-projects/spring-security/blob/96852e8860138a482cb13d1479573f24ff6443c6/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/PublicClientAuthenticationConverter.java#L47-L83","documentation":"PublicClientAuthenticationConverter (used for PKCE public clients) throws invalid_request when the client_id parameter is absent, empty, or appears more than once in the authorization/token request. Public clients authenticate via client_id plus code_verifier rather than a secret, so client_id is mandatory and must be a single value.","triggerScenarios":"A PKCE request (matched by presence of code_verifier path) without client_id, with client_id= (empty), or with client_id sent twice; thrown from convert() at the first validation check.","commonSituations":"SPA/mobile clients omitting client_id from the token exchange after redirect; authorization servers configured with PublicClientAuthenticationConverter while the client sends credentials via Basic auth instead; duplicated client_id when both query and body carry it; IDE-generated curl examples missing the parameter.","solutions":["Include exactly one client_id parameter matching a registered public client in the request.","Remove duplicate client_id occurrences (check both query string and form body).","Verify the converter is appropriate: if your client is confidential, use ClientSecretAuthenticationConverter/basic auth instead of the public-client path.","Log the request parameters before hitting the endpoint to confirm client_id presence and count."],"exampleFix":"// before\nPOST /oauth2/token\ngrant_type=authorization_code&code=abc&code_verifier=xyz   // no client_id\n// after\nPOST /oauth2/token\ngrant_type=authorization_code&code=abc&code_verifier=xyz&client_id=my-public-client","handlingStrategy":"validation","validationCode":"const p = new URLSearchParams(body);\nif (p.getAll('client_id').length !== 1 || !p.get('client_id')) {\n  throw new Error('public client requests require exactly one client_id');\n}","typeGuard":"function isPublicClientRequest(params) {\n  const ids = params.getAll('client_id');\n  return ids.length === 1 && ids[0].length > 0 && params.getAll('code_verifier').length === 1;\n}","tryCatchPattern":"try {\n  const res = await tokenRequest({ clientId, code, codeVerifier });\n} catch (e) {\n  if (e.error === 'invalid_request') {\n    console.error('Check client_id and code_verifier are present exactly once');\n  }\n}","preventionTips":["Store the registered client_id in a single config value and always append it to token/authorize requests.","Check both query string and body for accidental duplicates of client_id.","If the client has a secret, do not route it through the public-client converter; use basic/secret authentication instead.","Add a request-interceptor test asserting client_id appears exactly once."],"tags":["oauth2","pkce","public-client","client-id","invalid-request"],"backgroundTag":"missing-required-argument","analyzedSha":"96852e8860138a482cb13d1479573f24ff6443c6","analyzedAt":"2026-09-10T23:25:23.477Z","contentChangedAt":"2026-09-10T23:25:23.477Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}