Backfill test for token endpoint error when JWK is not yet available

Signed-off-by: Ryan Richard <richardry@vmware.com>
This commit is contained in:
Aram Price 2020-12-07 11:53:24 -08:00 committed by Ryan Richard
parent e0b6133bf1
commit 648fa4b9ba
4 changed files with 77 additions and 16 deletions

View File

@ -50,7 +50,7 @@ func (s *dynamicOpenIDConnectECDSAStrategy) GenerateIDToken(
_, activeJwk := s.jwksProvider.GetJWKS(s.fositeConfig.IDTokenIssuer) _, activeJwk := s.jwksProvider.GetJWKS(s.fositeConfig.IDTokenIssuer)
if activeJwk == nil { if activeJwk == nil {
plog.Debug("no JWK found for issuer", "issuer", s.fositeConfig.IDTokenIssuer) 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) key, ok := activeJwk.Key.(*ecdsa.PrivateKey)
if !ok { if !ok {
@ -65,7 +65,7 @@ func (s *dynamicOpenIDConnectECDSAStrategy) GenerateIDToken(
"actualType", "actualType",
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) return compose.NewOpenIDConnectECDSAStrategy(s.fositeConfig, key).GenerateIDToken(ctx, requester)

View File

@ -9,6 +9,7 @@ import (
"crypto/elliptic" "crypto/elliptic"
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"errors"
"net/url" "net/url"
"testing" "testing"
@ -42,7 +43,8 @@ func TestDynamicOpenIDConnectECDSAStrategy(t *testing.T) {
name string name string
issuer string issuer string
jwksProvider func(jwks.DynamicJWKSProvider) jwksProvider func(jwks.DynamicJWKSProvider)
wantError string wantErrorType *fosite.RFC6749Error
wantErrorCause string
wantSigningJWK *jose.JSONWebKey wantSigningJWK *jose.JSONWebKey
}{ }{
{ {
@ -63,9 +65,10 @@ func TestDynamicOpenIDConnectECDSAStrategy(t *testing.T) {
}, },
}, },
{ {
name: "jwks provider does not contain signing key for issuer", name: "jwks provider does not contain signing key for issuer",
issuer: goodIssuer, issuer: goodIssuer,
wantError: "no JWK found for issuer", wantErrorType: fosite.ErrTemporarilyUnavailable,
wantErrorCause: "no JWK found for issuer",
}, },
{ {
name: "jwks provider contains signing key of wrong type 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 { for _, test := range tests {
@ -111,8 +115,9 @@ func TestDynamicOpenIDConnectECDSAStrategy(t *testing.T) {
}, },
} }
idToken, err := s.GenerateIDToken(context.Background(), requester) idToken, err := s.GenerateIDToken(context.Background(), requester)
if test.wantError != "" { if test.wantErrorType != nil {
require.EqualError(t, err, test.wantError) require.True(t, errors.Is(err, test.wantErrorType))
require.EqualError(t, err.(*fosite.RFC6749Error).Cause(), test.wantErrorCause)
} else { } else {
require.NoError(t, err) require.NoError(t, err)

View File

@ -183,6 +183,15 @@ var (
"status_code": 400 "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) { func TestTokenEndpoint(t *testing.T) {
@ -214,7 +223,18 @@ func TestTokenEndpoint(t *testing.T) {
}, },
authCode string, 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 wantStatus int
wantBodyFields []string wantBodyFields []string
@ -381,6 +401,12 @@ func TestTokenEndpoint(t *testing.T) {
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantExactBody: fositeWrongPKCEVerifierErrorBody, 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 { for _, test := range tests {
test := test test := test
@ -393,8 +419,16 @@ func TestTokenEndpoint(t *testing.T) {
client := fake.NewSimpleClientset() client := fake.NewSimpleClientset()
secrets := client.CoreV1().Secrets("some-namespace") secrets := client.CoreV1().Secrets("some-namespace")
var oauthHelper fosite.OAuth2Provider
var authCode string
var jwtSigningKey *ecdsa.PrivateKey
oauthStore := oidc.NewKubeStorage(secrets) 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 { if test.storage != nil {
test.storage(t, oauthStore, authCode) test.storage(t, oauthStore, authCode)
@ -595,7 +629,30 @@ func makeHappyOauthHelper(
jwtSigningKey, jwkProvider := generateJWTSigningKeyAndJWKSProvider(t, goodIssuer) jwtSigningKey, jwkProvider := generateJWTSigningKeyAndJWKSProvider(t, goodIssuer)
oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, []byte(hmacSecret), jwkProvider) 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. // 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. // 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) authResponder, err := oauthHelper.NewAuthorizeResponse(ctx, authRequester, session)
require.NoError(t, err) require.NoError(t, err)
return authResponder
return oauthHelper, authResponder.GetCode(), jwtSigningKey
} }
func generateJWTSigningKeyAndJWKSProvider(t *testing.T, issuer string) (*ecdsa.PrivateKey, jwks.DynamicJWKSProvider) { func generateJWTSigningKeyAndJWKSProvider(t *testing.T, issuer string) (*ecdsa.PrivateKey, jwks.DynamicJWKSProvider) {

View File

@ -68,7 +68,7 @@ func TestSupervisorLogin(t *testing.T) {
return proxyURL, nil return proxyURL, nil
}, },
}} }}
oidcHttpClientContext := oidc.ClientContext(ctx, httpClient) oidcHTTPClientContext := oidc.ClientContext(ctx, httpClient)
// Use the CA to issue a TLS server cert. // Use the CA to issue a TLS server cert.
t.Logf("issuing test certificate") t.Logf("issuing test certificate")
@ -111,7 +111,7 @@ func TestSupervisorLogin(t *testing.T) {
// Perform OIDC discovery for our downstream. // Perform OIDC discovery for our downstream.
var discovery *oidc.Provider var discovery *oidc.Provider
assert.Eventually(t, func() bool { assert.Eventually(t, func() bool {
discovery, err = oidc.NewProvider(oidcHttpClientContext, downstream.Spec.Issuer) discovery, err = oidc.NewProvider(oidcHTTPClientContext, downstream.Spec.Issuer)
return err == nil return err == nil
}, 30*time.Second, 200*time.Millisecond) }, 30*time.Second, 200*time.Millisecond)
require.NoError(t, err) require.NoError(t, err)
@ -164,7 +164,7 @@ func TestSupervisorLogin(t *testing.T) {
require.NotEmpty(t, authcode) require.NotEmpty(t, authcode)
// Call the token endpoint to get tokens. // 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) require.NoError(t, err)
// Verify the ID Token. // Verify the ID Token.