[Bug]: --oidc-groups-claim ignored for tokens from --extra-jwt-issuers (groups claim hardcoded)

Author: jaewakCreated Sep 17, 2026Updated Sep 17, 2026

OAuth2-Proxy Version

v7.15.4 (also present on master)

Provider

oidc

Expected Behaviour

When --oidc-groups-claim is configured to a non-default claim name (e.g. ADGroups), the groups claim should be honored consistently for all verified JWT bearer tokens including tokens minted by issuers configured via --extra-jwt-issuers (used together with --skip-jwt-bearer-tokens).

A bearer token from an extra JWT issuer that carries an allowed group under the configured custom claim name should populate SessionState.Groups, and therefore pass --allowed-group authorization.

Current Behaviour

For tokens verified through an extra JWT issuer, the groups claim name is hardcoded to groups and ignores --oidc-groups-claim.

The session loaders for extra JWT issuers are built directly from CreateTokenToSessionFunc in oauthproxy.go:

go
for _, verifier := range opts.GetJWTBearerVerifiers() {
    sessionLoaders = append(sessionLoaders,
        middlewareapi.CreateTokenToSessionFunc(verifier.Verify))
}

CreateTokenToSessionFunc unmarshals claims into a struct with a hardcoded JSON tag (pkg/apis/middleware/session.go):

go
var claims struct {
    Subject           string   `json:"sub"`
    Email             string   `json:"email"`
    Verified          *bool    `json:"email_verified"`
    PreferredUsername string   `json:"preferred_username"`
    Groups            []string `json:"groups"`   // <-- hardcoded, ignores --oidc-groups-claim
}

Because the verifiers returned by GetJWTBearerVerifiers() only carry issuer/audience/JWKS information, there is no way to plumb a custom groups claim name through this path.

Consequently, if a token from an extra JWT issuer carries its groups under a custom claim (e.g. ADGroups), SessionState.Groups is left empty. Authorize (providers/provider_default.go) then finds no matching group and rejects the request, even though the token genuinely contains an allowed group.

By contrast, tokens verified through the main OIDC provider are unaffected: OIDCProvider.CreateSessionFromTokenbuildSessionFromClaims reads the configurable p.GroupsClaim (--oidc-groups-claim), so a custom claim name works correctly there.

The failure is silent, which makes it hard to debug

Critically, this misconfiguration produces no error and no log line. The token is valid, signature verification passes, the session is created successfully. It simply has empty Groups. From there the request is denied at the authorization step with a generic 403, indistinguishable from a token that legitimately lacks the required group.

There is nothing in the oauth2-proxy logs pointing at the groups claim being the culprit. In practice this meant debugging down through the Kubernetes layer to eventually discover that the ADGroups claim was being dropped, when the real cause was a hardcoded claim name one config option should have controlled. A user reasonably expects --oidc-groups-claim to apply to every JWT path, so the mismatch is very unintuitive to track down.

Steps To Reproduce

  1. Configure oauth2-proxy with the oidc provider and --oidc-groups-claim=ADGroups.
  2. Enable bearer-token auth with --skip-jwt-bearer-tokens and add a second trusted issuer via --extra-jwt-issuers=https://issuer.example.com=my-audience.
  3. Restrict access with --allowed-group=my-trusted-group.
  4. Mint a valid JWT from the extra issuer whose groups are carried under the ADGroups claim, including my-trusted-group.
  5. Send a request: Authorization: Bearer <token>.
  6. Observe the request is rejected (403) with no indicative log line, because SessionState.Groups is empty — the ADGroups claim was ignored and only a groups claim would have been read.

Possible Solutions

Make the groups claim name used by CreateTokenToSessionFunc configurable rather than hardcoded, so the extra-JWT-issuer path honors --oidc-groups-claim (falling back to groups by default). Options include:

  • Extract the groups claim generically (similar to the OIDC provider's claim extractor / p.GroupsClaim) instead of relying on a fixed struct tag.
  • Thread the configured groups claim name into the session loaders built for GetJWTBearerVerifiers() in buildSessionChain.

This also aligns with an existing maintainer intent. providers/provider_data.go already carries a TODO to move away from hardcoded claim mapping:

go
// TODO (@NickMeves) Deprecate for dynamic claim to session mapping
{"preferred_username", &ss.PreferredUsername},

so a generic/dynamic claim-to-session mapping in this path would be consistent with the direction the project already wants to take.

Happy to open a PR — wanted to confirm the preferred approach first.

Configuration details or additional information

  • This affects the --extra-jwt-issuers + --skip-jwt-bearer-tokens code path only; the primary OIDC provider path already respects --oidc-groups-claim.
  • Relevant code:
    • oauthproxy.gobuildSessionChain, extra JWT verifier loop
    • pkg/apis/middleware/session.goCreateTokenToSessionFunc
    • providers/provider_data.gobuildSessionFromClaims (the working, dynamic path for comparison) and the @NickMeves dynamic-claim-mapping TODO

Source: oauth2-proxy/oauth2-proxy