[Bug]: dot-separated --oidc-groups-claim paths are ignored for tokens from extra JWT issuers

Author: jaewakCreated Sep 23, 2026Updated Sep 23, 2026

OAuth2-Proxy Version

v7.15.4, also present on master (checked at 3847c799)

Provider

oidc

Expected Behaviour

--oidc-groups-claim takes either a plain claim name or a dot separated path. The main OIDC path resolves it that way through getClaimFrom in pkg/providers/util/claim_extractor.go, which tries the value as a key first and then as a JSON path.

A token verified through --extra-jwt-issuers should resolve the same option the same way, so --oidc-groups-claim=realm_access.roles should pick up groups from a nested claim on that path too.

Current Behaviour

The extra JWT issuer path only does a top level key lookup. extractGroups in pkg/apis/middleware/session.go reads the claim out of a map[string]json.RawMessage, so a name containing a dot never matches.

Given --oidc-groups-claim=realm_access.roles and a token carrying:

json
{"sub": "123", "realm_access": {"roles": ["foo", "bar"]}}

the primary issuer resolves groups to ["foo", "bar"] and an extra issuer resolves none.

Nothing is logged and no error is returned, because the key is just absent from the map, and an absent groups claim is treated as a user who belongs to no groups. The request then fails --allowed-group and returns a generic 403.

That is the same silent failure described in #3539. #3539 was about the claim name being hardcoded and #3540 fixed that part. Nested claim names are still affected.

Steps To Reproduce

  1. Configure the oidc provider with --oidc-groups-claim=realm_access.roles.
  2. Enable bearer token auth with --skip-jwt-bearer-tokens and add --extra-jwt-issuers=https://issuer.example.com=my-audience.
  3. Set --allowed-group=foo.
  4. Mint a token from the extra issuer carrying realm_access: {"roles": ["foo"]}.
  5. Send it as Authorization: Bearer <token>.
  6. The request is rejected with a 403 and nothing in the logs points at the groups claim. The same token from the primary issuer is accepted.

Possible Solutions

Resolve the claim with the same extractor the main path uses, so util.NewClaimExtractor(ctx, token, nil, nil) followed by GetClaim(groupsClaim). pkg/providers/util does not import pkg/apis/middleware, so there is no import cycle, and a nil profile URL is handled.

There is a decision to make before doing that. The two paths disagree on what to do with a badly shaped claim. After #3540, extractGroups returns an error when the claim is present but is neither a string nor an array of strings. CoerceClaim on the main path is lenient instead. It turns ["foo", 1] into ["foo", "1"], and it turns an object into a single group whose name is the JSON text of that object.

Reusing GetClaimInto would quietly adopt the lenient behaviour on the extra issuer path. Keeping the stricter check would mean calling GetClaim and type switching the raw value, because a check layered on top of CoerceClaim would never fire.

This is authorization input, so it seems worth deciding on purpose rather than inheriting whichever behaviour comes for free.

Configuration details or additional information

Split out of #3540 at a reviewer's request, so the gap stays visible after that PR merges. Related to #3539.

Relevant code:

  • pkg/apis/middleware/session.go, extractGroups
  • pkg/providers/util/claim_extractor.go, getClaimFrom
  • pkg/util/util.go, CoerceClaim and toStringSlice

Source: oauth2-proxy/oauth2-proxy