diff --git a/internal/oidc/dynamic_open_id_connect_ecdsa_strategy.go b/internal/oidc/dynamic_open_id_connect_ecdsa_strategy.go index 582b9cc6..302044db 100644 --- a/internal/oidc/dynamic_open_id_connect_ecdsa_strategy.go +++ b/internal/oidc/dynamic_open_id_connect_ecdsa_strategy.go @@ -50,7 +50,7 @@ func (s *dynamicOpenIDConnectECDSAStrategy) GenerateIDToken( _, activeJwk := s.jwksProvider.GetJWKS(s.fositeConfig.IDTokenIssuer) if activeJwk == nil { plog.Debug("no JWK found for issuer", "issuer", s.fositeConfig.IDTokenIssuer) - return "", constable.Error("no JWK found for issuer") + return "", fosite.ErrTemporarilyUnavailable.WithCause(constable.Error("no JWK found for issuer")) } key, ok := activeJwk.Key.(*ecdsa.PrivateKey) if !ok { @@ -65,7 +65,7 @@ func (s *dynamicOpenIDConnectECDSAStrategy) GenerateIDToken( "actualType", actualType, ) - return "", constable.Error("JWK must be of type ecdsa") + return "", fosite.ErrServerError.WithCause(constable.Error("JWK must be of type ecdsa")) } return compose.NewOpenIDConnectECDSAStrategy(s.fositeConfig, key).GenerateIDToken(ctx, requester) diff --git a/internal/oidc/dynamic_open_id_connect_ecdsa_strategy_test.go b/internal/oidc/dynamic_open_id_connect_ecdsa_strategy_test.go index 1b48f471..85a63501 100644 --- a/internal/oidc/dynamic_open_id_connect_ecdsa_strategy_test.go +++ b/internal/oidc/dynamic_open_id_connect_ecdsa_strategy_test.go @@ -9,6 +9,7 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/rsa" + "errors" "net/url" "testing" @@ -42,7 +43,8 @@ func TestDynamicOpenIDConnectECDSAStrategy(t *testing.T) { name string issuer string jwksProvider func(jwks.DynamicJWKSProvider) - wantError string + wantErrorType *fosite.RFC6749Error + wantErrorCause string wantSigningJWK *jose.JSONWebKey }{ { @@ -63,9 +65,10 @@ func TestDynamicOpenIDConnectECDSAStrategy(t *testing.T) { }, }, { - name: "jwks provider does not contain signing key for issuer", - issuer: goodIssuer, - wantError: "no JWK found for issuer", + name: "jwks provider does not contain signing key for issuer", + issuer: goodIssuer, + wantErrorType: fosite.ErrTemporarilyUnavailable, + wantErrorCause: "no JWK found for issuer", }, { name: "jwks provider contains signing key of wrong type for issuer", @@ -80,7 +83,8 @@ func TestDynamicOpenIDConnectECDSAStrategy(t *testing.T) { }, ) }, - wantError: "JWK must be of type ecdsa", + wantErrorType: fosite.ErrServerError, + wantErrorCause: "JWK must be of type ecdsa", }, } for _, test := range tests { @@ -111,8 +115,9 @@ func TestDynamicOpenIDConnectECDSAStrategy(t *testing.T) { }, } idToken, err := s.GenerateIDToken(context.Background(), requester) - if test.wantError != "" { - require.EqualError(t, err, test.wantError) + if test.wantErrorType != nil { + require.True(t, errors.Is(err, test.wantErrorType)) + require.EqualError(t, err.(*fosite.RFC6749Error).Cause(), test.wantErrorCause) } else { require.NoError(t, err) diff --git a/internal/oidc/token/token_handler_test.go b/internal/oidc/token/token_handler_test.go index a56a649c..7a3e2ffe 100644 --- a/internal/oidc/token/token_handler_test.go +++ b/internal/oidc/token/token_handler_test.go @@ -183,6 +183,15 @@ var ( "status_code": 400 } `) + + fositeTemporarilyUnavailableErrorBody = here.Doc(` + { + "error": "temporarily_unavailable", + "error_description": "The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server", + "error_verbose": "The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server", + "status_code": 503 + } + `) ) func TestTokenEndpoint(t *testing.T) { @@ -214,7 +223,18 @@ func TestTokenEndpoint(t *testing.T) { }, authCode string, ) - request func(r *http.Request, authCode string) + request func(r *http.Request, authCode string) + makeOathHelper func( + t *testing.T, + authRequest *http.Request, + store interface { + oauth2.TokenRevocationStorage + oauth2.CoreStorage + openid.OpenIDConnectRequestStorage + pkce.PKCERequestStorage + fosite.ClientManager + }, + ) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) wantStatus int wantBodyFields []string @@ -381,6 +401,12 @@ func TestTokenEndpoint(t *testing.T) { wantStatus: http.StatusBadRequest, wantExactBody: fositeWrongPKCEVerifierErrorBody, }, + { + name: "private signing key for JWTs has not yet been provided by the controller who is responsible for dynamically providing it", + makeOathHelper: makeOauthHelperWithNilPrivateJWTSigningKey, + wantStatus: http.StatusServiceUnavailable, + wantExactBody: fositeTemporarilyUnavailableErrorBody, + }, } for _, test := range tests { test := test @@ -393,8 +419,16 @@ func TestTokenEndpoint(t *testing.T) { client := fake.NewSimpleClientset() secrets := client.CoreV1().Secrets("some-namespace") + var oauthHelper fosite.OAuth2Provider + var authCode string + var jwtSigningKey *ecdsa.PrivateKey + oauthStore := oidc.NewKubeStorage(secrets) - oauthHelper, authCode, jwtSigningKey := makeHappyOauthHelper(t, authRequest, oauthStore) + if test.makeOathHelper != nil { + oauthHelper, authCode, jwtSigningKey = test.makeOathHelper(t, authRequest, oauthStore) + } else { + oauthHelper, authCode, jwtSigningKey = makeHappyOauthHelper(t, authRequest, oauthStore) + } if test.storage != nil { test.storage(t, oauthStore, authCode) @@ -595,7 +629,30 @@ func makeHappyOauthHelper( jwtSigningKey, jwkProvider := generateJWTSigningKeyAndJWKSProvider(t, goodIssuer) oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, []byte(hmacSecret), jwkProvider) + authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper) + return oauthHelper, authResponder.GetCode(), jwtSigningKey +} +func makeOauthHelperWithNilPrivateJWTSigningKey( + t *testing.T, + authRequest *http.Request, + store interface { + oauth2.TokenRevocationStorage + oauth2.CoreStorage + openid.OpenIDConnectRequestStorage + pkce.PKCERequestStorage + fosite.ClientManager + }, +) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) { + t.Helper() + + jwkProvider := jwks.NewDynamicJWKSProvider() // empty provider which contains no signing key for this issuer + oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, []byte(hmacSecret), jwkProvider) + authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper) + return oauthHelper, authResponder.GetCode(), nil +} + +func simulateAuthEndpointHavingAlreadyRun(t *testing.T, authRequest *http.Request, oauthHelper fosite.OAuth2Provider) fosite.AuthorizeResponder { // Simulate the auth endpoint running so Fosite code will fill the store with realistic values. // // We only set the fields in the session that Fosite wants us to set. @@ -616,8 +673,7 @@ func makeHappyOauthHelper( } authResponder, err := oauthHelper.NewAuthorizeResponse(ctx, authRequester, session) require.NoError(t, err) - - return oauthHelper, authResponder.GetCode(), jwtSigningKey + return authResponder } func generateJWTSigningKeyAndJWKSProvider(t *testing.T, issuer string) (*ecdsa.PrivateKey, jwks.DynamicJWKSProvider) { diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index da8c10fd..65e90791 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -68,7 +68,7 @@ func TestSupervisorLogin(t *testing.T) { return proxyURL, nil }, }} - oidcHttpClientContext := oidc.ClientContext(ctx, httpClient) + oidcHTTPClientContext := oidc.ClientContext(ctx, httpClient) // Use the CA to issue a TLS server cert. t.Logf("issuing test certificate") @@ -111,7 +111,7 @@ func TestSupervisorLogin(t *testing.T) { // Perform OIDC discovery for our downstream. var discovery *oidc.Provider assert.Eventually(t, func() bool { - discovery, err = oidc.NewProvider(oidcHttpClientContext, downstream.Spec.Issuer) + discovery, err = oidc.NewProvider(oidcHTTPClientContext, downstream.Spec.Issuer) return err == nil }, 30*time.Second, 200*time.Millisecond) require.NoError(t, err) @@ -164,7 +164,7 @@ func TestSupervisorLogin(t *testing.T) { require.NotEmpty(t, authcode) // Call the token endpoint to get tokens. - tokenResponse, err := downstreamOAuth2Config.Exchange(oidcHttpClientContext, authcode, pkceParam.Verifier()) + tokenResponse, err := downstreamOAuth2Config.Exchange(oidcHTTPClientContext, authcode, pkceParam.Verifier()) require.NoError(t, err) // Verify the ID Token.