mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-09-06 16:17:08 +00:00
Perform an upstream refresh during downstream refresh for OIDC upstreams
- If the upstream refresh fails, then fail the downstream refresh - If the upstream refresh returns an ID token, then validate it (we use its claims in the future, but not in this commit) - If the upstream refresh returns a new refresh token, then save it into the user's session in storage - Pass the provider cache into the token handler so it can use the cached providers to perform upstream refreshes - Handle unexpected errors in the token handler where the user's session does not contain the expected data. These should not be possible in practice unless someone is manually editing the storage, but handle them anyway just to be safe. - Refactor to share the refresh code between the CLI and the token endpoint by moving it into the UpstreamOIDCIdentityProviderI interface, since the token endpoint needed it to be part of that interface anyway
This commit is contained in:
@@ -808,9 +808,9 @@ func (h *handlerState) tokenExchangeRFC8693(baseToken *oidctypes.Token) (*oidcty
|
||||
|
||||
func (h *handlerState) handleRefresh(ctx context.Context, refreshToken *oidctypes.RefreshToken) (*oidctypes.Token, error) {
|
||||
h.logger.V(debugLogLevel).Info("Pinniped: Refreshing cached token.")
|
||||
refreshSource := h.oauth2Config.TokenSource(ctx, &oauth2.Token{RefreshToken: refreshToken.Token})
|
||||
upstreamOIDCIdentityProvider := h.getProvider(h.oauth2Config, h.provider, h.httpClient)
|
||||
|
||||
refreshed, err := refreshSource.Token()
|
||||
refreshed, err := upstreamOIDCIdentityProvider.PerformRefresh(ctx, refreshToken.Token)
|
||||
if err != nil {
|
||||
// Ignore errors during refresh, but return nil which will trigger the full login flow.
|
||||
return nil, nil
|
||||
@@ -818,7 +818,7 @@ func (h *handlerState) handleRefresh(ctx context.Context, refreshToken *oidctype
|
||||
|
||||
// The spec is not 100% clear about whether an ID token from the refresh flow should include a nonce, and at least
|
||||
// some providers do not include one, so we skip the nonce validation here (but not other validations).
|
||||
return h.getProvider(h.oauth2Config, h.provider, h.httpClient).ValidateToken(ctx, refreshed, "")
|
||||
return upstreamOIDCIdentityProvider.ValidateToken(ctx, refreshed, "")
|
||||
}
|
||||
|
||||
func (h *handlerState) handleAuthCodeCallback(w http.ResponseWriter, r *http.Request) (err error) {
|
||||
|
||||
@@ -35,6 +35,7 @@ import (
|
||||
"go.pinniped.dev/internal/oidc/provider"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
"go.pinniped.dev/internal/testutil/testlogger"
|
||||
"go.pinniped.dev/internal/upstreamoidc"
|
||||
"go.pinniped.dev/pkg/oidcclient/nonce"
|
||||
"go.pinniped.dev/pkg/oidcclient/oidctypes"
|
||||
"go.pinniped.dev/pkg/oidcclient/pkce"
|
||||
@@ -404,11 +405,17 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
||||
clientID: "test-client-id",
|
||||
opt: func(t *testing.T) Option {
|
||||
return func(h *handlerState) error {
|
||||
h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI {
|
||||
h.getProvider = func(config *oauth2.Config, provider *oidc.Provider, client *http.Client) provider.UpstreamOIDCIdentityProviderI {
|
||||
mock := mockUpstream(t)
|
||||
mock.EXPECT().
|
||||
ValidateToken(gomock.Any(), HasAccessToken(testToken.AccessToken.Token), nonce.Nonce("")).
|
||||
Return(&testToken, nil)
|
||||
mock.EXPECT().
|
||||
PerformRefresh(gomock.Any(), testToken.RefreshToken.Token).
|
||||
DoAndReturn(func(ctx context.Context, refreshToken string) (*oauth2.Token, error) {
|
||||
// Call the real production code to perform a refresh.
|
||||
return upstreamoidc.New(config, provider, client).PerformRefresh(ctx, refreshToken)
|
||||
})
|
||||
return mock
|
||||
}
|
||||
|
||||
@@ -445,11 +452,17 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
||||
clientID: "test-client-id",
|
||||
opt: func(t *testing.T) Option {
|
||||
return func(h *handlerState) error {
|
||||
h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI {
|
||||
h.getProvider = func(config *oauth2.Config, provider *oidc.Provider, client *http.Client) provider.UpstreamOIDCIdentityProviderI {
|
||||
mock := mockUpstream(t)
|
||||
mock.EXPECT().
|
||||
ValidateToken(gomock.Any(), HasAccessToken(testToken.AccessToken.Token), nonce.Nonce("")).
|
||||
Return(nil, fmt.Errorf("some validation error"))
|
||||
mock.EXPECT().
|
||||
PerformRefresh(gomock.Any(), "test-refresh-token-returning-invalid-id-token").
|
||||
DoAndReturn(func(ctx context.Context, refreshToken string) (*oauth2.Token, error) {
|
||||
// Call the real production code to perform a refresh.
|
||||
return upstreamoidc.New(config, provider, client).PerformRefresh(ctx, refreshToken)
|
||||
})
|
||||
return mock
|
||||
}
|
||||
|
||||
@@ -1522,11 +1535,17 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
||||
})
|
||||
h.cache = cache
|
||||
|
||||
h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI {
|
||||
h.getProvider = func(config *oauth2.Config, provider *oidc.Provider, client *http.Client) provider.UpstreamOIDCIdentityProviderI {
|
||||
mock := mockUpstream(t)
|
||||
mock.EXPECT().
|
||||
ValidateToken(gomock.Any(), HasAccessToken(testToken.AccessToken.Token), nonce.Nonce("")).
|
||||
Return(&testToken, nil)
|
||||
mock.EXPECT().
|
||||
PerformRefresh(gomock.Any(), testToken.RefreshToken.Token).
|
||||
DoAndReturn(func(ctx context.Context, refreshToken string) (*oauth2.Token, error) {
|
||||
// Call the real production code to perform a refresh.
|
||||
return upstreamoidc.New(config, provider, client).PerformRefresh(ctx, refreshToken)
|
||||
})
|
||||
return mock
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user