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

  1. Send the access token (not the refresh token) in the Authorization: Bearer header
  2. Use the refresh token only against the token endpoint with grant_type=refresh_token to obtain a new access token
  3. 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

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


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/b65204b598b97b63. Report an issue: GitHub.