feat(oidc): support refresh token flow without id_token#3151
Conversation
- 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>
✅ Deploy Preview for pinniped-dev canceled.
|
|
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! |
Some OIDC providers (e.g. AuthGate) do not return an
id_tokenin their refresh token response.Pinniped's
handleRefresh()previously required anid_tokenunconditionally, causing silentrefresh 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_tokenwhen building theExecCredentialresponse.Changes:
--skip-require-id-token-on-refreshflag topinniped login oidc: when set, the refreshflow calls
ValidateTokenAndMergeWithUserInfowithrequireIDToken=falseand returns theaccess_tokenas the credential whenid_tokenis absent--oidc-skip-require-id-token-on-refreshflag topinniped get kubeconfig: automaticallyembeds
--skip-require-id-token-on-refreshinto the generated kubeconfig exec argsWithRequireIDTokenOnRefresh(bool)Option topkg/oidcclient; defaults totruetopreserve existing behavior for all other providers
tokenCredential()to accept*oidctypes.Tokenand fall back toAccessTokenwhenIDTokenis nil; extractapplyTokenToStatus()helper to remove duplicated logic--skip-require-id-token-on-refreshis usedtogether with
--enable-concierge(incompatible combination)OIDCClientOptionsinterface and its GoMock implementationRelease note: