{"record":{"id":"4197672e746a46a8","repo":"quarkusio/quarkus","slug":"refresh-token-values-are-not-equal","errorCode":null,"errorMessage":"Refresh token values are not equal","messagePattern":"Refresh token values are not equal","errorType":"http","errorClass":"OIDCException","httpStatus":500,"severity":"error","filePath":"integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java","lineNumber":262,"sourceCode":"        return \"tenant-split-tokens:\" + getAccessToken();\n    }\n\n    @GET\n    @Path(\"access/tenant-split-id-refresh-token\")\n    public String getAccessIdRefreshTokenSplitTokens() {\n        return \"tenant-split-id-refresh-token:\" + getAccessToken();\n    }\n\n    @GET\n    @Path(\"refresh\")\n    public String getRefreshToken() {\n        return doGetRefreshToken(true);\n    }\n\n    private String doGetRefreshToken(boolean refreshWithAccessTokenCheckRequired) {\n        if (refreshWithAccessTokenCheckRequired && refreshToken.getToken() != null\n                && !accessTokenCredential.getRefreshToken().getToken().equals(refreshToken.getToken())) {\n            throw new OIDCException(\"Refresh token values are not equal\");\n        }\n        if (refreshToken.getToken() != null && !refreshToken.getToken().isEmpty()) {\n            String message = \"RT injected\";\n            String listenerMessage = routingContext.get(\"listener-message\");\n            if (listenerMessage != null) {\n                message += (\"(\" + listenerMessage + \")\");\n            }\n            return message;\n        } else {\n            return \"no refresh\";\n        }\n    }\n\n    @GET\n    @Path(\"refresh/tenant-idtoken-only\")\n    public String getRefreshTokenIdTokenOnly() {\n        return \"tenant-idtoken-only:\" + getRefreshToken();\n    }","sourceCodeStart":244,"sourceCodeEnd":280,"githubUrl":"https://github.com/quarkusio/quarkus/blob/e1c734241f34c7919086ceb4c9262b4a58f6de44/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java#L244-L280","documentation":"Invariant check shared by the refresh-token endpoints via doGetRefreshToken. When an access-token check is required and a refresh token is present, the injected TokenCredential's refresh token must equal the injected @RefreshToken value; otherwise the OIDC extension injected inconsistent refresh-token representations and OIDCException is thrown.","triggerScenarios":"A refresh-token endpoint calls doGetRefreshToken(true) while refreshToken.getToken() != null and accessTokenCredential.getRefreshToken().getToken() differs from refreshToken.getToken().","commonSituations":"A concurrent token refresh replaced one representation but not the other; split-token tenant configuration causing one endpoint to see a refreshed token and the other a stale one; custom augmentation rewriting TokenCredential but not the @RefreshToken injection; regression in OidcIdentityProvider refresh-token propagation.","solutions":["Avoid triggering refresh and this check concurrently on the same session; let refresh complete first","Confirm split-tokens tenant properties are consistent so both injections come from the same token state","Ensure no custom augmentor rewrites TokenCredential refresh tokens without updating the @RefreshToken injection source","If stock config reproduces it, inspect OidcIdentityProvider refresh-token attribute population for a regression"],"exampleFix":"// before\nif (!accessTokenCredential.getRefreshToken().getToken().equals(refreshToken.getToken())) {\n    throw new OIDCException(\"Refresh token values are not equal\");\n}\n// after (null-safe guard first)\nString injected = accessTokenCredential.getRefreshToken() == null ? null : accessTokenCredential.getRefreshToken().getToken();\nif (refreshToken.getToken() != null && injected != null && !injected.equals(refreshToken.getToken())) {\n    throw new OIDCException(\"Refresh token values are not equal\");\n}","handlingStrategy":"validation","validationCode":"if (refreshToken.getToken() != null && accessTokenCredential.getRefreshToken() != null && !accessTokenCredential.getRefreshToken().getToken().equals(refreshToken.getToken())) { throw new IllegalStateException(\"refresh token mismatch\"); }","typeGuard":"boolean refreshTokensConsistent(RefreshToken rt, TokenCredential cred) { return rt.getToken() == null || cred == null || cred.getRefreshToken() == null || rt.getToken().equals(cred.getRefreshToken().getToken()); }","tryCatchPattern":"try { return doGetRefreshToken(true); } catch (OIDCException e) { log.warnf(\"refresh token mismatch: %s\", e.getMessage()); reauthenticate(); }","preventionTips":["Serialize token refresh operations per session","Keep split-tokens config identical across tenants","Do not mutate TokenCredential refresh tokens in augmentors","Verify refresh-token injection after any OIDC extension upgrade"],"tags":["oidc","refresh-token","quarkus","test-assertion"],"backgroundTag":"token-injection-mismatch","analyzedSha":"e1c734241f34c7919086ceb4c9262b4a58f6de44","analyzedAt":"2026-09-05T17:01:29.979Z","contentChangedAt":"2026-09-05T17:01:29.979Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}