{"record":{"id":"2ec2b01cc656d26e","repo":"spring-projects/spring-security","slug":"invalidated-device-code-used-by-registered-client","errorCode":null,"errorMessage":"Invalidated device code used by registered client '%s'","messagePattern":"Invalidated device code used by registered client '(.+?)'","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationProvider.java","lineNumber":133,"sourceCode":"\t\tif (authorization == null) {\n\t\t\tthrow new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);\n\t\t}\n\n\t\tif (this.logger.isTraceEnabled()) {\n\t\t\tthis.logger.trace(\"Retrieved authorization with device code\");\n\t\t}\n\n\t\tOAuth2Authorization.Token<OAuth2DeviceCode> deviceCode = authorization.getToken(OAuth2DeviceCode.class);\n\t\tAssert.notNull(deviceCode, \"deviceCode cannot be null\");\n\n\t\tif (!registeredClient.getId().equals(authorization.getRegisteredClientId())) {\n\t\t\tif (!deviceCode.isInvalidated()) {\n\t\t\t\t// Invalidate the device code given that a different client is attempting\n\t\t\t\t// to use it\n\t\t\t\tauthorization = OAuth2Authorization.from(authorization).invalidate(deviceCode.getToken()).build();\n\t\t\t\tthis.authorizationService.save(authorization);\n\t\t\t\tif (this.logger.isWarnEnabled()) {\n\t\t\t\t\tthis.logger.warn(LogMessage.format(\"Invalidated device code used by registered client '%s'\",\n\t\t\t\t\t\t\tauthorization.getRegisteredClientId()));\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);\n\t\t}\n\n\t\t// In https://www.rfc-editor.org/rfc/rfc8628.html#section-3.5,\n\t\t// the following error codes are defined:\n\n\t\t// expired_token\n\t\t// The \"device_code\" has expired, and the device authorization\n\t\t// session has concluded. The client MAY commence a new device\n\t\t// authorization request but SHOULD wait for user interaction before\n\t\t// restarting to avoid unnecessary polling.\n\t\tif (deviceCode.isExpired()) {\n\t\t\tif (!deviceCode.isInvalidated()) {\n\t\t\t\t// Invalidate the device code\n\t\t\t\tauthorization = OAuth2Authorization.from(authorization).invalidate(deviceCode.getToken()).build();","sourceCodeStart":115,"sourceCodeEnd":151,"githubUrl":"https://github.com/spring-projects/spring-security/blob/96852e8860138a482cb13d1479573f24ff6443c6/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationProvider.java#L115-L151","documentation":"Emitted by OAuth2DeviceCodeAuthenticationProvider when a client attempts to redeem a device code that was issued to a different client. The provider invalidates the device code to prevent further use, saves the authorization, logs this warning, and throws OAuth2AuthenticationException with INVALID_GRANT. This is a cross-client replay defense for the device authorization grant (RFC 8628).","triggerScenarios":"During the device access token request, the client_id of the caller does not match the registered client that originally received the device code (e.g. a different device code flow instance, a misconfigured client_id, or a code copied to another application).","commonSituations":"Copy-pasting a verification URL/device code between two apps configured against the same authorization server, an environment mismatch where staging and prod clients share a database, or a typo/wrong client_id in the token request.","solutions":["Ensure the token request uses exactly the same client_id that started the device authorization flow.","Check client configuration (application.yml / RegisteredClient settings) so the correct client_id is sent in the device token request.","Do not share device codes between applications; run the device flow again for the second client.","If tokens are unexpectedly revoked, restart the device authorization flow for the correct client."],"exampleFix":"// before\nString clientId = \"wrong-app\"; // hardcoded from another service\nclient.deviceAccessToken(code, clientId);\n// after\nString clientId = config.getOwnClientId();\nclient.deviceAccessToken(code, clientId);","handlingStrategy":"validation","validationCode":"// verify client identity before redeeming a device code\nif (!issuedClientId.equals(this.clientId)) {\n  throw new IllegalStateException(\"device code was issued to a different client; start your own device flow\");\n}","typeGuard":null,"tryCatchPattern":"try {\n  TokenResponse r = redeemDeviceCode(deviceCode);\n} catch (OAuth2AuthenticationException e) {\n  if (\"invalid_grant\".equals(e.getError().getErrorCode())) {\n    // code invalidated (cross-client use): restart the device authorization flow\n    startDeviceAuthorization();\n  }\n}","preventionTips":["Never share device codes or verification URIs across client applications","Keep client_id configuration consistent between authorization and token requests","Give each environment (dev/stage/prod) its own registered clients","On invalid_grant, restart the device flow rather than retrying the same code"],"tags":["oauth2","device-code","device-flow","invalid-grant","client-mismatch"],"backgroundTag":"oauth-token-exchange-failed","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"}