Skip to content

feat(oidc): support refresh token flow without id_token#3151

Open
appleboy wants to merge 1 commit into
vmware:mainfrom
appleboy:feature/skip-require-id-token-on-refresh
Open

feat(oidc): support refresh token flow without id_token#3151
appleboy wants to merge 1 commit into
vmware:mainfrom
appleboy:feature/skip-require-id-token-on-refresh

Conversation

@appleboy

@appleboy appleboy commented Jun 24, 2026

Copy link
Copy Markdown

Some OIDC providers (e.g. AuthGate) do not return an id_token in their refresh token response.
Pinniped's handleRefresh() previously required an id_token unconditionally, causing silent
refresh failures and triggering a browser-based re-login every time the token expired.

This PR adds an opt-in flag to allow the refresh flow to succeed without an id_token,
falling back to the access_token when building the ExecCredential response.

Changes:

  • Add --skip-require-id-token-on-refresh flag to pinniped login oidc: when set, the refresh
    flow calls ValidateTokenAndMergeWithUserInfo with requireIDToken=false and returns the
    access_token as the credential when id_token is absent
  • Add --oidc-skip-require-id-token-on-refresh flag to pinniped get kubeconfig: automatically
    embeds --skip-require-id-token-on-refresh into the generated kubeconfig exec args
  • Add WithRequireIDTokenOnRefresh(bool) Option to pkg/oidcclient; defaults to true to
    preserve existing behavior for all other providers
  • Extend tokenCredential() to accept *oidctypes.Token and fall back to AccessToken when
    IDToken is nil; extract applyTokenToStatus() helper to remove duplicated logic
  • Add nil guard with a clear error message when --skip-require-id-token-on-refresh is used
    together with --enable-concierge (incompatible combination)
  • Update OIDCClientOptions interface and its GoMock implementation
  • Add unit tests covering the new happy path, default behavior preservation, and refresh failure

Release note:

Add --skip-require-id-token-on-refresh flag to pinniped login oidc and
--oidc-skip-require-id-token-on-refresh flag to pinniped get kubeconfig.
When enabled, the OIDC refresh token flow no longer requires the provider to
return an id_token, and falls back to using the access_token as the cluster
credential. This is intended for use with non-Supervisor OIDC providers (such as
AuthGate) that omit id_token from their refresh token response. Default behavior
is unchanged. This flag is incompatible with --enable-concierge.

- add --skip-require-id-token-on-refresh flag to login oidc command
- add --oidc-skip-require-id-token-on-refresh flag to get kubeconfig, auto-embedded in exec args
- add WithRequireIDTokenOnRefresh(bool) Option; defaults to true for backward compatibility
- make handleRefresh() use the configurable requireIDToken value instead of hardcoded true
- extend tokenCredential() to accept *oidctypes.Token and fall back to AccessToken when IDToken is nil
- add nil guard with clear error when --enable-concierge is used without an id_token
- update OIDCClientOptions interface and mock with new method
- add unit tests covering happy path, default behavior, and error cases

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@netlify

netlify Bot commented Jun 24, 2026

Copy link
Copy Markdown

Deploy Preview for pinniped-dev canceled.

Name Link
🔨 Latest commit 225ffe5
🔍 Latest deploy log https://app.netlify.com/projects/pinniped-dev/deploys/6a3b815618f7c50008486f88

@cfryanr

cfryanr commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Hi @appleboy, thanks for submitting a PR. I would like to better understand your use case.

In the OIDC specification, the provider is not required to return a new refresh token on each refresh grant. See https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse.

However, if their original ID token is expired, and if the refresh grant does not return a new ID token which is not expired, then the user's session is effectively ended. The user no longer holds any valid proof of identity when their only ID token has expired and they can't get a new one via refreshing. The user's only recourse is to start a new session so they can get a new ID token.

Generally speaking, in OIDC, the user's access token is not proof of identity. It is a bearer token that can be used for authorization (authZ, not authN).

That's why the Pinniped CLI works the way that it does. It will keep sending the initial ID token from the original login to Kubernetes until the token is expired. Then it will attempt a refresh to see if it can get a new ID token. If it can, it will send the new ID token to Kubernetes for authentication. If it can't get a new ID token, then it no longer holds any valid proof of identity for the user, so the only option that it has is to help the user start all over from the beginning again.

I'm not familiar with AuthGate. Could you please explain how it would be different from what I described above? Thanks!

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.

2 participants