Skip to content

Answer token endpoint failures with the RFC 6749 error codes, and look up the UserInfo client by azp - #282

Merged
longsleep merged 2 commits into
libregraph:masterfrom
longsleep-io:upstream/6-client-errors
Oct 14, 2026
Merged

longsleep merged 2 commits into
libregraph:masterfrom
longsleep-io:upstream/6-client-errors

Conversation

@longsleep

Copy link
Copy Markdown
Collaborator

Two fixes in how the token and UserInfo endpoints identify the client and report failures.

Token endpoint error codes

The token endpoint answered two kinds of failure with error codes RFC 6749 section 5.2 does not assign to them. What a client does next depends on the code, so it took the wrong action.

  • A refresh token which is malformed, expired or signed with a key lico does not hold was answered with invalid_request. It now answers invalid_grant, which tells the client to sign in again rather than to fix its request.
  • A failed client lookup, for an unknown client_id or a wrong secret alike, was answered with access_denied, which belongs to the authorization endpoint. It now answers invalid_client. oidc-go does not define that code, so it is added in oidc/errors.go.
  • A client which authenticated with the Authorization header gets invalid_client with status 401 and WWW-Authenticate: Basic realm="<issuer>", as section 5.2 asks. Credentials sent in the body keep the 400.

This matters most for pre-registered, long-lived clients: they outlive refresh tokens and key rotations, and a mistyped secret is how an operator usually meets invalid_client.

UserInfo client lookup

UserInfo took the client from the first aud value. Since #273, a token bound to a resource carries the resource as aud and the client in azp. For such a token:

  • the registration lookup missed, so a client registered with userinfo_signed_response_alg got unsigned JSON;
  • the identity backend was told that the resource was the client.

UserInfo now uses azp when present, as every other lookup of a client by token already does.

Tests and docs

  • TestTokenRefresh gains an expired and a malformed refresh token, both expecting invalid_grant.
  • The new TestTokenClientAuthentication covers an unknown client and a wrong secret, each sent in the body and in the header.
  • TestAuthorizeAndTokenWithResource gains a subtest expecting signed user info for a resource-bound token. Without the fix it gets unsigned JSON.
  • The API spec documents which failures answer invalid_grant and which invalid_client, the 401, and the UserInfo behaviour.

A refresh token which failed to parse, verify or validate, being
malformed, expired or signed with a key lico does not hold, was
answered with invalid_request. RFC 6749 section 5.2 assigns that case
to invalid_grant, and the difference is what a client acts on: an
invalid grant means the user has to sign in again, an invalid request
means the request itself is broken. TokenRequest.Validate now answers
with it.

A failed client lookup at the token endpoint, for an unknown client_id
or a wrong secret alike, was answered with access_denied, which is a
code of the authorization endpoint (RFC 6749 section 4.1.2.1). It now
answers invalid_client. oidc-go does not define that code, so it is
added as ErrorCodeOAuth2InvalidClient in oidc/errors.go.

Section 5.2 also asks that a client which authenticated through the
Authorization header be answered with 401 and a WWW-Authenticate
challenge of the same scheme. TokenRequest gains
ClientAuthenticationBasic, set when the Basic header supplied the
credentials, and in that case TokenHandler answers an invalid_client
with 401 and WWW-Authenticate: Basic realm="<issuer>". Credentials sent
in the body keep the 400.

This matters once clients are long-lived and pre-registered rather
than minted per connection by dynamic registration. Such clients
outlive their refresh tokens and key rotations, and a wrong secret
pasted into a client dialog is the likeliest way an operator meets
invalid_client.

The API spec now documents which failures of the token endpoint answer
invalid_grant and which invalid_client, adds invalid_client to the
OIDCError enum, and describes the 401 with WWW-Authenticate: Basic.

TestTokenRefresh gains an expired and a malformed refresh token, both
expecting invalid_grant. The new TestTokenClientAuthentication covers
an unknown client and a wrong secret, each sent in the body and in the
header, for which the test provider gains a confidential client with a
secret.
UserInfoHandler took the client from claims.Audience[0], both for the
p.clients.Get lookup that decides whether user info is answered signed
and for the LookupContext.Audience it hands to the identity manager,
which documents that value as the client ID. Since resource indicators
(RFC 8707) a token bound to a resource carries the resource as its
audience and the client in azp. For such a token the registration
lookup missed, so a client which had asked for
userinfo_signed_response_alg was answered with unsigned JSON, and the
backend was told that the resource was the client.

Both now use claims.AuthorizedPartyOrAudience(), as every other lookup
of a client by token already does. This also matters for a later
change: client IDs which are URLs will be resolved by fetching them,
and a lookup by audience would then request the resource URL whenever
its host is allowed. The API spec now says that userinfo accepts
resource-bound tokens and takes their client from azp.

TestAuthorizeAndTokenWithResource gains a subtest which gives the test
client a signed user info algorithm, obtains a resource-bound token and
expects signed user info whose aud is the client. Without the fix it
receives unsigned JSON.
@longsleep
longsleep merged commit de2f585 into libregraph:master Oct 14, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant