ory/hydra · error
Only access tokens are allowed in the authorization header.
Error message
Only access tokens are allowed in the authorization header.
What it means
Hydra's OIDC userinfo handler rejects requests whose bearer token is not a fosite access token. If the token type presented in the Authorization header is e.g. an ID token, refresh token, or some other token type, the handler responds 401 with `WWW-Authenticate: Bearer error="invalid_token"` and this description. Only access tokens carry the userinfo authorization grant.
Source
Thrown at oauth2/handler.go:667
// Extensions:
// x-ory-ratelimit-bucket: hydra-public-medium
func (h *Handler) getOidcUserInfo(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
session := NewSessionWithCustomClaims(ctx, h.c, "")
tokenType, ar, err := h.r.OAuth2Provider().IntrospectToken(ctx, fosite.AccessTokenFromRequest(r), fosite.AccessToken, session)
if err != nil {
rfcerr := fosite.ErrorToRFC6749Error(err)
if rfcerr.StatusCode() == http.StatusUnauthorized {
w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Bearer error="%s",error_description="%s"`, rfcerr.ErrorField, rfcerr.GetDescription()))
}
h.r.Writer().WriteError(w, r, err)
return
}
if tokenType != fosite.AccessToken {
errorDescription := "Only access tokens are allowed in the authorization header."
w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Bearer error="invalid_token",error_description="%s"`, errorDescription))
h.r.Writer().WriteErrorCode(w, r, http.StatusUnauthorized, errors.New(errorDescription))
return
}
c, ok := ar.GetClient().(*client.Client)
if !ok {
h.r.Writer().WriteError(w, r, errors.WithStack(fosite.ErrServerError.WithHint("Unable to type assert to *client.Client.")))
return
}
interim := ar.GetSession().(*Session).IDTokenClaims().ToMap()
delete(interim, "nonce")
delete(interim, "at_hash")
delete(interim, "c_hash")
delete(interim, "exp")
delete(interim, "sid")
delete(interim, "jti")
aud, ok := interim["aud"].([]string)View on GitHub (pinned to 4174065ffb)
Solutions
- Send the `access_token` value from the token endpoint response, not the `id_token`.
- Re-run the authorization code / client credentials flow and capture `access_token` from the JSON response.
- Check client code that picks the token field from the response body and fix the key it uses.
- If the token is expired/revoked, obtain a fresh access token.
Example fix
// before
const token = tokenResponse.id_token
fetch('https://hydra.example.com/userinfo', { headers: { Authorization: `Bearer ${token}` } })
// after
const token = tokenResponse.access_token
fetch('https://hydra.example.com/userinfo', { headers: { Authorization: `Bearer ${token}` } }) Defensive patterns
Strategy: validation
Validate before calling
// client-side: only send the access_token to /userinfo
if tokenResponse.access_token == "" {
return errors.New("no access_token in token response; cannot call userinfo")
}
req.Header.Set("Authorization", "Bearer "+tokenResponse.access_token) Type guard
function isAccessTokenResponse(r: { token_type?: string; access_token?: string }): r is { token_type: 'bearer'; access_token: string } {
return !!r.access_token && (r.token_type?.toLowerCase() === 'bearer');
} Try / catch
// handle 401 invalid_token by re-authenticating
const res = await fetch(userInfoURL, { headers: { Authorization: `Bearer ${accessToken}` } });
if (res.status === 401) {
accessToken = await renewAccessToken(); // do NOT fall back to id_token
return fetchUserInfo(accessToken);
} Prevention
- Store access_token and id_token as distinct fields and never interchange them
- Read token_type from the token response and assert it is 'bearer'
- Refresh access tokens before expiry instead of reusing ID tokens
- Add integration tests hitting /userinfo with the real flow
When it happens
Trigger: GET/POST to /userinfo with `Authorization: Bearer <token>` where the token fails the `tokenType != fosite.AccessToken` check — classically an ID token pasted instead of an access token, or an opaque/refresh token from the token response's other fields.
Common situations: Client apps confusing `id_token` with `access_token` in the OIDC token response; frontends storing only the ID token; tests hitting /userinfo with a JWT from a different grant; using a token obtained from a non-token-endpoint flow.
Related errors
- invalid flow state: expected one of %v, got %d
- flow Subject %s does not match the HandledLoginRequest Subje
- flow ForceSubjectIdentifier %s does not match the HandledLog
- issuer URL must be set unless development mode is enabled
- issuer URL scheme must be HTTPS unless development mode is e
AI-assisted analysis of ory/hydra@4174065ffb (2026-09-03).
Data as JSON: /api/errors/b22ea2ee4fa24f65.
Report an issue: GitHub.