{"record":{"id":"b65204b598b97b63","repo":"quarkusio/quarkus","slug":"refresh-token-can-only-be-used-with-the-refresh-to","errorCode":null,"errorMessage":"Refresh token can only be used with the refresh token grant","messagePattern":"Refresh token can only be used with the refresh token grant","errorType":"exception","errorClass":"io.quarkus.oidc.runtime.OIDCException","httpStatus":null,"severity":"error","filePath":"extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java","lineNumber":499,"sourceCode":"            builder.addAttribute(INTROSPECTION_ATTRIBUTE, introspectionResult);\n        }\n    }\n\n    public static void setSecurityIdentityConfigMetadata(QuarkusSecurityIdentity.Builder builder,\n            TenantConfigContext resolvedContext) {\n        if (resolvedContext.provider().client != null) {\n            builder.addAttribute(CONFIG_METADATA_ATTRIBUTE, resolvedContext.provider().client.getMetadata());\n        }\n    }\n\n    public static void validatePrimaryJwtTokenType(Token tokenConfig, JsonObject tokenJson) {\n        if (tokenJson.containsKey(\"typ\")) {\n            String type = tokenJson.getString(\"typ\");\n            if (tokenConfig.tokenType().isPresent() && !tokenConfig.tokenType().get().equals(type)) {\n                throw new OIDCException(\"Invalid token type\");\n            } else if (\"Refresh\".equals(type)) {\n                // At least check it is not a refresh token issued by Keycloak\n                throw new OIDCException(\"Refresh token can only be used with the refresh token grant\");\n            }\n        }\n    }\n\n    static Uni<Void> removeSessionCookie(RoutingContext context, OidcTenantConfig oidcConfig,\n            TokenStateManager tokenStateManager) {\n        List<String> cookieNames = context.get(SESSION_COOKIE_NAME);\n        if (cookieNames != null) {\n            LOG.debugf(\"Remove session cookie names: %s\", cookieNames);\n            StringBuilder cookieValue = new StringBuilder();\n            for (String cookieName : cookieNames) {\n                cookieValue.append(removeCookie(context, oidcConfig, cookieName));\n            }\n            return tokenStateManager.deleteTokens(context, oidcConfig, cookieValue.toString(),\n                    deleteTokensRequestContext);\n        } else {\n            return VOID_UNI;\n        }","sourceCodeStart":481,"sourceCodeEnd":517,"githubUrl":"https://github.com/quarkusio/quarkus/blob/e1c734241f34c7919086ceb4c9262b4a58f6de44/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java#L481-L517","documentation":"Quarkus OIDC rejects Keycloak refresh tokens (JWT with typ 'Refresh') when they are presented as authentication tokens, because refresh tokens may only be used with the refresh-token grant. OidcUtils.validatePrimaryTokenType throws this to stop refresh tokens being validated as bearer credentials.","triggerScenarios":"A Keycloak-issued refresh token (JWT whose typ header is 'Refresh') is sent as a bearer/primary token and validated against an OidcTenantConfig whose tokenType does not accept it; e.g. a client accidentally passes the refresh_token instead of access_token in the Authorization header.","commonSituations":"Client code storing Keycloak's token response and mixing up refresh_token and access_token fields; offline/session tokens typed 'Refresh' sent to services; misconfigured SPAs persisting the wrong token.","solutions":["Send the access token (not the refresh token) in the Authorization: Bearer header","Use the refresh token only against the token endpoint with grant_type=refresh_token to obtain a new access token","If the IdP wrongly types access tokens as 'Refresh', fix the client/IdP token mapping"],"exampleFix":"// before\nAuthorization: Bearer <refresh_token>\n// after\nAuthorization: Bearer <access_token>","handlingStrategy":"validation","validationCode":"if (\"Refresh\".equals(decodedJwt.getHeader(\"typ\"))) {\n    // use it only for the refresh-token grant, never as a bearer token\n    throw new IllegalArgumentException(\"Use refresh_token only with grant_type=refresh_token\");\n}","typeGuard":"boolean isRefreshToken(String typ) { return \"Refresh\".equals(typ); }","tryCatchPattern":"try { validate(token); } catch (OIDCException e) { throw new UnauthorizedException(\"A refresh token was sent; obtain a new access token via the token endpoint\"); }","preventionTips":["Store access_token and refresh_token in distinct variables in client code","Perform refresh via the token endpoint only","Log token typ headers (never full tokens) to catch mix-ups early"],"tags":["oidc","keycloak","refresh-token","token-validation"],"backgroundTag":"refresh-token-misuse","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"}