quarkusio/quarkus · error · io.quarkus.oidc.runtime.OIDCException
Refresh token can only be used with the refresh token grant
Error message
Refresh token can only be used with the refresh token grant
What it means
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.
Source
Thrown at extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java:499
builder.addAttribute(INTROSPECTION_ATTRIBUTE, introspectionResult);
}
}
public static void setSecurityIdentityConfigMetadata(QuarkusSecurityIdentity.Builder builder,
TenantConfigContext resolvedContext) {
if (resolvedContext.provider().client != null) {
builder.addAttribute(CONFIG_METADATA_ATTRIBUTE, resolvedContext.provider().client.getMetadata());
}
}
public static void validatePrimaryJwtTokenType(Token tokenConfig, JsonObject tokenJson) {
if (tokenJson.containsKey("typ")) {
String type = tokenJson.getString("typ");
if (tokenConfig.tokenType().isPresent() && !tokenConfig.tokenType().get().equals(type)) {
throw new OIDCException("Invalid token type");
} else if ("Refresh".equals(type)) {
// At least check it is not a refresh token issued by Keycloak
throw new OIDCException("Refresh token can only be used with the refresh token grant");
}
}
}
static Uni<Void> removeSessionCookie(RoutingContext context, OidcTenantConfig oidcConfig,
TokenStateManager tokenStateManager) {
List<String> cookieNames = context.get(SESSION_COOKIE_NAME);
if (cookieNames != null) {
LOG.debugf("Remove session cookie names: %s", cookieNames);
StringBuilder cookieValue = new StringBuilder();
for (String cookieName : cookieNames) {
cookieValue.append(removeCookie(context, oidcConfig, cookieName));
}
return tokenStateManager.deleteTokens(context, oidcConfig, cookieValue.toString(),
deleteTokensRequestContext);
} else {
return VOID_UNI;
}View on GitHub (pinned to e1c734241f)
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
Example fix
// before Authorization: Bearer <refresh_token> // after Authorization: Bearer <access_token>
Defensive patterns
Strategy: validation
Validate before calling
if ("Refresh".equals(decodedJwt.getHeader("typ"))) {
// use it only for the refresh-token grant, never as a bearer token
throw new IllegalArgumentException("Use refresh_token only with grant_type=refresh_token");
} Type guard
boolean isRefreshToken(String typ) { return "Refresh".equals(typ); } Try / catch
try { validate(token); } catch (OIDCException e) { throw new UnauthorizedException("A refresh token was sent; obtain a new access token via the token endpoint"); } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Application 'web-app' type is only supported if access token
- Failed to parse the realm name.
- Failed to find a matching OidcTenantConfig for tenant:
- Required ID token is not returned in the refresh token grant
- DPoP access token hash does not match the DPoP proof access
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/b65204b598b97b63.
Report an issue: GitHub.