Unit tests for token endpoint for custom ID token lifetimes

This commit is contained in:
Ryan Richard
2024-04-18 09:48:21 -07:00
parent 5dbf05c31d
commit a1efcefdce
5 changed files with 177 additions and 41 deletions

View File

@@ -613,7 +613,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo
addFullyCapableDynamicClientAndSecretToKubeResources := func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
oidcClient, secret := testutil.FullyCapableOIDCClientAndStorageSecret(t,
"some-namespace", dynamicClientID, dynamicClientUID, downstreamRedirectURI,
"some-namespace", dynamicClientID, dynamicClientUID, downstreamRedirectURI, nil,
[]string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
require.NoError(t, kubeClient.Tracker().Add(secret))

View File

@@ -171,7 +171,7 @@ func TestCallbackEndpoint(t *testing.T) {
addFullyCapableDynamicClientAndSecretToKubeResources := func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
oidcClient, secret := testutil.FullyCapableOIDCClientAndStorageSecret(t,
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, downstreamRedirectURI,
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, downstreamRedirectURI, nil,
[]string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
require.NoError(t, kubeClient.Tracker().Add(secret))
@@ -815,7 +815,7 @@ func TestCallbackEndpoint(t *testing.T) {
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID,
[]configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude username scope)
[]configv1alpha1.Scope{"openid", "offline_access", "groups"}, // username not allowed
downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
downstreamRedirectURI, nil, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
require.NoError(t, kubeClient.Tracker().Add(secret))
},
@@ -858,7 +858,7 @@ func TestCallbackEndpoint(t *testing.T) {
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID,
[]configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude groups scope)
[]configv1alpha1.Scope{"openid", "offline_access", "username"}, // groups not allowed
downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
downstreamRedirectURI, nil, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
require.NoError(t, kubeClient.Tracker().Add(secret))
},
@@ -1094,7 +1094,7 @@ func TestCallbackEndpoint(t *testing.T) {
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID,
[]configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude username scope)
[]configv1alpha1.Scope{"openid", "offline_access", "groups"}, // username not allowed
downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
downstreamRedirectURI, nil, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
require.NoError(t, kubeClient.Tracker().Add(secret))
},
@@ -1123,7 +1123,7 @@ func TestCallbackEndpoint(t *testing.T) {
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID,
[]configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude groups scope)
[]configv1alpha1.Scope{"openid", "offline_access", "username"}, // groups not allowed
downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
downstreamRedirectURI, nil, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
require.NoError(t, kubeClient.Tracker().Add(secret))
},

View File

@@ -280,7 +280,7 @@ func TestPostLoginEndpoint(t *testing.T) {
addFullyCapableDynamicClientAndSecretToKubeResources := func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
oidcClient, secret := testutil.FullyCapableOIDCClientAndStorageSecret(t,
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, downstreamRedirectURI,
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, downstreamRedirectURI, nil,
[]string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
require.NoError(t, kubeClient.Tracker().Add(secret))
@@ -615,7 +615,7 @@ func TestPostLoginEndpoint(t *testing.T) {
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID,
[]configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude username scope)
[]configv1alpha1.Scope{"openid", "offline_access", "groups"}, // username not allowed
downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
downstreamRedirectURI, nil, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
require.NoError(t, kubeClient.Tracker().Add(secret))
},
@@ -649,7 +649,7 @@ func TestPostLoginEndpoint(t *testing.T) {
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID,
[]configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude groups scope)
[]configv1alpha1.Scope{"openid", "offline_access", "username"}, // groups not allowed
downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
downstreamRedirectURI, nil, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
require.NoError(t, kubeClient.Tracker().Add(secret))
},
@@ -693,7 +693,7 @@ func TestPostLoginEndpoint(t *testing.T) {
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID,
[]configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude groups scope)
[]configv1alpha1.Scope{"openid", "offline_access", "username"}, // groups not allowed
downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
downstreamRedirectURI, nil, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
require.NoError(t, kubeClient.Tracker().Add(secret))
},
@@ -1053,7 +1053,7 @@ func TestPostLoginEndpoint(t *testing.T) {
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID,
[]configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude username scope)
[]configv1alpha1.Scope{"openid", "offline_access", "groups"}, // username not allowed
downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
downstreamRedirectURI, nil, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
require.NoError(t, kubeClient.Tracker().Add(secret))
},
@@ -1073,7 +1073,7 @@ func TestPostLoginEndpoint(t *testing.T) {
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID,
[]configv1alpha1.GrantType{"authorization_code", "refresh_token"}, // token exchange not allowed (required to exclude groups scope)
[]configv1alpha1.Scope{"openid", "offline_access", "username"}, // groups not allowed
downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
downstreamRedirectURI, nil, []string{testutil.HashedPassword1AtGoMinCost}, oidcclientvalidator.Validate)
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
require.NoError(t, kubeClient.Tracker().Add(secret))
},

View File

@@ -38,6 +38,7 @@ import (
"k8s.io/apiserver/pkg/warning"
"k8s.io/client-go/kubernetes/fake"
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/utils/ptr"
"k8s.io/utils/strings/slices"
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
@@ -291,6 +292,14 @@ type tokenEndpointResponseExpectedValues struct {
wantCustomSessionDataStored *psession.CustomSessionData
wantWarnings []RecordedWarning
wantAdditionalClaims map[string]interface{}
// The expected lifetime of the ID tokens issued by authcode exchange and refresh, but not token exchange.
// When zero, will assume that the test wants the default value for ID token lifetime.
wantIDTokenLifetimeSeconds int
}
func withWantCustomIDTokenLifetime(wantIDTokenLifetimeSeconds int, w tokenEndpointResponseExpectedValues) tokenEndpointResponseExpectedValues {
w.wantIDTokenLifetimeSeconds = wantIDTokenLifetimeSeconds
return w
}
type authcodeExchangeInputs struct {
@@ -308,6 +317,7 @@ func addFullyCapableDynamicClientAndSecretToKubeResources(t *testing.T, supervis
dynamicClientID,
dynamicClientUID,
goodRedirectURI,
nil, // no custom ID token lifetime
[]string{testutil.HashedPassword1AtGoMinCost, testutil.HashedPassword2AtGoMinCost},
oidcclientvalidator.Validate,
)
@@ -315,6 +325,22 @@ func addFullyCapableDynamicClientAndSecretToKubeResources(t *testing.T, supervis
require.NoError(t, kubeClient.Tracker().Add(secret))
}
func addFullyCapableDynamicClientWithCustomIDTokenLifetimeAndSecretToKubeResources(idTokenLifetime int32) func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
return func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
oidcClient, secret := testutil.FullyCapableOIDCClientAndStorageSecret(t,
"some-namespace",
dynamicClientID,
dynamicClientUID,
goodRedirectURI,
ptr.To(idTokenLifetime), // with custom ID token lifetime
[]string{testutil.HashedPassword1AtGoMinCost, testutil.HashedPassword2AtGoMinCost},
oidcclientvalidator.Validate,
)
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
require.NoError(t, kubeClient.Tracker().Add(secret))
}
}
func modifyAuthcodeTokenRequestWithDynamicClientAuth(r *http.Request, authCode string) {
r.Body = happyAuthcodeRequestBody(authCode).WithClientID("").ReadCloser() // No client_id in body.
r.SetBasicAuth(dynamicClientID, testutil.PlaintextPassword1) // Use basic auth header instead.
@@ -403,6 +429,27 @@ func TestTokenEndpointAuthcodeExchange(t *testing.T) {
},
},
},
{
name: "request is valid and tokens are issued for dynamic client which has a custom ID token lifetime",
kubeResources: addFullyCapableDynamicClientWithCustomIDTokenLifetimeAndSecretToKubeResources(4242),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) {
addDynamicClientIDToFormPostBody(r)
r.Form.Set("scope", "openid pinniped:request-audience username groups")
},
modifyTokenRequest: modifyAuthcodeTokenRequestWithDynamicClientAuth,
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK,
wantClientID: dynamicClientID,
wantSuccessBodyFields: []string{"id_token", "access_token", "token_type", "scope", "expires_in"}, // no refresh token
wantRequestedScopes: []string{"openid", "pinniped:request-audience", "username", "groups"},
wantGrantedScopes: []string{"openid", "pinniped:request-audience", "username", "groups"},
wantUsername: goodUsername,
wantGroups: goodGroups,
wantIDTokenLifetimeSeconds: 4242,
},
},
},
{
name: "request is valid and tokens are issued for dynamic client with additional claims",
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
@@ -972,14 +1019,16 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
wantGroups: goodGroups,
}
successfulAuthCodeExchangeUsingDynamicClient := tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK,
wantClientID: dynamicClientID,
wantSuccessBodyFields: []string{"id_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "pinniped:request-audience", "username", "groups"},
wantGrantedScopes: []string{"openid", "pinniped:request-audience", "username", "groups"},
wantUsername: goodUsername,
wantGroups: goodGroups,
successfulAuthCodeExchangeUsingDynamicClient := func() tokenEndpointResponseExpectedValues {
return tokenEndpointResponseExpectedValues{
wantStatus: http.StatusOK,
wantClientID: dynamicClientID,
wantSuccessBodyFields: []string{"id_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "pinniped:request-audience", "username", "groups"},
wantGrantedScopes: []string{"openid", "pinniped:request-audience", "username", "groups"},
wantUsername: goodUsername,
wantGroups: goodGroups,
}
}
doValidAuthCodeExchange := authcodeExchangeInputs{
@@ -989,13 +1038,15 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
want: successfulAuthCodeExchange,
}
doValidAuthCodeExchangeUsingDynamicClient := authcodeExchangeInputs{
modifyAuthRequest: func(authRequest *http.Request) {
addDynamicClientIDToFormPostBody(authRequest)
authRequest.Form.Set("scope", "openid pinniped:request-audience username groups")
},
modifyTokenRequest: modifyAuthcodeTokenRequestWithDynamicClientAuth,
want: successfulAuthCodeExchangeUsingDynamicClient,
doValidAuthCodeExchangeUsingDynamicClient := func() authcodeExchangeInputs {
return authcodeExchangeInputs{
modifyAuthRequest: func(authRequest *http.Request) {
addDynamicClientIDToFormPostBody(authRequest)
authRequest.Form.Set("scope", "openid pinniped:request-audience username groups")
},
modifyTokenRequest: modifyAuthcodeTokenRequestWithDynamicClientAuth,
want: successfulAuthCodeExchangeUsingDynamicClient(),
}
}
tests := []struct {
@@ -1081,7 +1132,27 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
{
name: "happy path with dynamic client",
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
authcodeExchange: doValidAuthCodeExchangeUsingDynamicClient,
authcodeExchange: doValidAuthCodeExchangeUsingDynamicClient(),
modifyRequestParams: func(t *testing.T, params url.Values) {
params.Del("client_id") // client auth for dynamic clients must be in basic auth header
},
modifyRequestHeaders: func(r *http.Request) {
r.SetBasicAuth(dynamicClientID, testutil.PlaintextPassword1)
},
requestedAudience: "some-workload-cluster",
wantStatus: http.StatusOK,
},
{
name: "happy path with dynamic client which has a custom ID token lifetime configuration (which does not apply to ID tokens from token exchanges)",
kubeResources: addFullyCapableDynamicClientWithCustomIDTokenLifetimeAndSecretToKubeResources(4242),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: doValidAuthCodeExchangeUsingDynamicClient().modifyAuthRequest,
modifyTokenRequest: doValidAuthCodeExchangeUsingDynamicClient().modifyTokenRequest,
want: withWantCustomIDTokenLifetime(
4242, // want custom lifetime for authcode exchange (but not for token exchange)
doValidAuthCodeExchangeUsingDynamicClient().want,
),
},
modifyRequestParams: func(t *testing.T, params url.Values) {
params.Del("client_id") // client auth for dynamic clients must be in basic auth header
},
@@ -1403,7 +1474,7 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
{
name: "dynamic client uses wrong client secret",
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
authcodeExchange: doValidAuthCodeExchangeUsingDynamicClient,
authcodeExchange: doValidAuthCodeExchangeUsingDynamicClient(),
modifyRequestParams: func(t *testing.T, params url.Values) {
params.Del("client_id") // client auth for dynamic clients must be in basic auth header
},
@@ -1418,7 +1489,7 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
{
name: "dynamic client uses wrong auth method (must use basic auth)",
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
authcodeExchange: doValidAuthCodeExchangeUsingDynamicClient,
authcodeExchange: doValidAuthCodeExchangeUsingDynamicClient(),
modifyRequestParams: func(t *testing.T, params url.Values) {
// Dynamic clients do not support this method of auth.
params.Set("client_id", dynamicClientID)
@@ -1604,6 +1675,7 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
// at and expires at dates which are newer than the old tokens.
time.Sleep(1 * time.Second)
// Perform the token exchange.
approxRequestTime := time.Now()
subject.ServeHTTP(rsp, req)
t.Logf("response: %#v", rsp)
@@ -1699,12 +1771,21 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
// Also assert which are the different from the original downstream ID token.
requireClaimsAreNotEqual(t, "jti", claimsOfFirstIDToken, tokenClaims) // JWT ID
requireClaimsAreNotEqual(t, "aud", claimsOfFirstIDToken, tokenClaims) // audience
requireClaimsAreNotEqual(t, "exp", claimsOfFirstIDToken, tokenClaims) // expires at
require.Greater(t, tokenClaims["exp"], claimsOfFirstIDToken["exp"])
requireClaimsAreNotEqual(t, "iat", claimsOfFirstIDToken, tokenClaims) // issued at
require.Greater(t, tokenClaims["iat"], claimsOfFirstIDToken["iat"])
requireClaimsAreNotEqual(t, "exp", claimsOfFirstIDToken, tokenClaims) // expires at
if test.authcodeExchange.want.wantIDTokenLifetimeSeconds == 0 {
// If the ID token lifetime of the original ID token was not customized by configuration,
// then both the original and new ID tokens should have default 2-minute lifetimes, with the
// clock starting for each at token issuing time. Therefore, the new one should expire
// after the original one (i.e. a moving 2-minute window).
require.Greater(t, tokenClaims["exp"], claimsOfFirstIDToken["exp"])
}
// Assert that the timestamps in the token are approximately as expected.
// When dynamic clients are configured to have a custom ID token lifetime, that does not apply to
// token exchanges. Therefore, we can always assert that the lifetime of the new ID token is always
// the default lifetime.
expiresAtAsFloat, ok := tokenClaims["exp"].(float64)
require.True(t, ok, "expected exp claim to be a float64")
expiresAt := time.Unix(int64(expiresAtAsFloat), 0)
@@ -1713,6 +1794,9 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
require.True(t, ok, "expected iat claim to be a float64")
issuedAt := time.Unix(int64(issuedAtAsFloat), 0)
testutil.RequireTimeInDelta(t, approxRequestTime.UTC(), issuedAt, timeComparisonFudge)
// The difference between iat (issued at) and exp (expires at) claims should be exactly the lifetime seconds.
require.Equal(t, int64(idTokenExpirationSeconds), int64(expiresAtAsFloat)-int64(issuedAtAsFloat),
"ID token lifetime was not the expected value")
// Assert that nothing in storage has been modified.
newSecrets, err := secrets.List(context.Background(), metav1.ListOptions{})
@@ -2280,6 +2364,42 @@ func TestRefreshGrant(t *testing.T) {
)),
},
},
{
name: "happy path refresh grant with openid scope granted (id token returned) using dynamic client which has custom ID token lifetime configured",
idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(
upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{
IDToken: &oidctypes.IDToken{
Claims: map[string]interface{}{
"sub": goodUpstreamSubject,
},
},
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
kubeResources: addFullyCapableDynamicClientWithCustomIDTokenLifetimeAndSecretToKubeResources(4242),
authcodeExchange: authcodeExchangeInputs{
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
modifyAuthRequest: func(r *http.Request) {
addDynamicClientIDToFormPostBody(r)
r.Form.Set("scope", "openid offline_access username groups")
},
modifyTokenRequest: modifyAuthcodeTokenRequestWithDynamicClientAuth,
want: withWantCustomIDTokenLifetime(4242,
withWantDynamicClientID(
happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
),
),
},
refreshRequest: refreshRequestInputs{
modifyTokenRequest: modifyRefreshTokenRequestWithDynamicClientAuth,
want: withWantCustomIDTokenLifetime(4242,
withWantDynamicClientID(
happyRefreshTokenResponseForOpenIDAndOfflineAccess(
upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken),
refreshedUpstreamTokensWithIDAndRefreshTokens(),
),
),
),
},
},
{
name: "happy path refresh grant with openid scope granted (id token returned) using dynamic client with additional claims",
idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(
@@ -4581,22 +4701,25 @@ func exchangeAuthcodeForTokens(
kubeResources(t, supervisorClient, kubeClient)
}
var oauthHelper fosite.OAuth2Provider
// Use the same timeouts configuration as the production code will use.
timeoutsConfiguration := oidc.DefaultOIDCTimeoutsConfiguration()
// Use lower minimum required bcrypt cost than we would use in production to keep unit the tests fast.
oauthStore = storage.NewKubeStorage(secrets, oidcClientsClient, oidc.DefaultOIDCTimeoutsConfiguration(), bcrypt.MinCost)
oauthStore = storage.NewKubeStorage(secrets, oidcClientsClient, timeoutsConfiguration, bcrypt.MinCost)
if test.makeJwksSigningKeyAndProvider == nil {
test.makeJwksSigningKeyAndProvider = generateJWTSigningKeyAndJWKSProvider
}
var oauthHelper fosite.OAuth2Provider
// Note that makeHappyOauthHelper() calls simulateAuthEndpointHavingAlreadyRun() to preload the session storage.
oauthHelper, authCode, jwtSigningKey = makeHappyOauthHelper(t, authRequest, oauthStore, test.makeJwksSigningKeyAndProvider, test.customSessionData, test.modifySession)
subject = NewHandler(
idps,
oauthHelper,
func(accessRequest fosite.AccessRequester) (bool, time.Duration) { return false, 0 },
func(accessRequest fosite.AccessRequester) (bool, time.Duration) { return false, 0 },
timeoutsConfiguration.OverrideDefaultAccessTokenLifespan,
timeoutsConfiguration.OverrideDefaultIDTokenLifespan,
)
authorizeEndpointGrantedOpenIDScope := strings.Contains(authRequest.Form.Get("scope"), "openid")
@@ -4674,7 +4797,7 @@ func requireTokenEndpointBehavior(
expectedNumberOfRefreshTokenSessionsStored = 1
}
if wantIDToken {
requireValidIDToken(t, parsedResponseBody, jwtSigningKey, test.wantClientID, wantNonceValueInIDToken, test.wantUsername, test.wantGroups, test.wantAdditionalClaims, parsedResponseBody["access_token"].(string), requestTime)
requireValidIDToken(t, parsedResponseBody, jwtSigningKey, test.wantClientID, wantNonceValueInIDToken, test.wantUsername, test.wantGroups, test.wantAdditionalClaims, test.wantIDTokenLifetimeSeconds, parsedResponseBody["access_token"].(string), requestTime)
}
if wantRefreshToken {
requireValidRefreshTokenStorage(t, parsedResponseBody, oauthStore, test.wantClientID, test.wantRequestedScopes, test.wantGrantedScopes, test.wantUsername, test.wantGroups, test.wantCustomSessionDataStored, test.wantAdditionalClaims, secrets, requestTime)
@@ -5198,6 +5321,7 @@ func requireValidIDToken(
wantUsernameInIDToken string,
wantGroupsInIDToken []string,
wantAdditionalClaims map[string]interface{},
wantIDTokenLifetimeSeconds int,
actualAccessToken string,
requestTime time.Time,
) {
@@ -5266,11 +5390,19 @@ func requireValidIDToken(
require.Empty(t, claims.Nonce)
}
if wantIDTokenLifetimeSeconds == 0 {
// When not specified, assert that the ID token has the default lifetime for an ID token.
wantIDTokenLifetimeSeconds = idTokenExpirationSeconds
}
// The difference between iat (issued at) and exp (expires at) claims should be exactly the lifetime seconds.
require.Equal(t, int64(wantIDTokenLifetimeSeconds), claims.ExpiresAt-claims.IssuedAt, "ID token lifetime was not the expected value")
expiresAt := time.Unix(claims.ExpiresAt, 0)
issuedAt := time.Unix(claims.IssuedAt, 0)
requestedAt := time.Unix(claims.RequestedAt, 0)
authTime := time.Unix(claims.AuthTime, 0)
testutil.RequireTimeInDelta(t, requestTime.UTC().Add(idTokenExpirationSeconds*time.Second), expiresAt, timeComparisonFudge)
testutil.RequireTimeInDelta(t, requestTime.UTC().Add(time.Duration(wantIDTokenLifetimeSeconds)*time.Second), expiresAt, timeComparisonFudge)
testutil.RequireTimeInDelta(t, requestTime.UTC(), issuedAt, timeComparisonFudge)
testutil.RequireTimeInDelta(t, goodRequestedAtTime, requestedAt, timeComparisonFudge)
testutil.RequireTimeInDelta(t, goodAuthTime, authTime, timeComparisonFudge)

View File

@@ -1,4 +1,4 @@
// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved.
// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package testutil
@@ -50,6 +50,7 @@ func newOIDCClient(
redirectURI string,
allowedGrantTypes []configv1alpha1.GrantType,
allowedScopes []configv1alpha1.Scope,
tokenLifetimesIDTokenSeconds *int32,
) *configv1alpha1.OIDCClient {
return &configv1alpha1.OIDCClient{
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: clientID, Generation: 1, UID: types.UID(clientUID)},
@@ -57,6 +58,7 @@ func newOIDCClient(
AllowedGrantTypes: allowedGrantTypes,
AllowedScopes: allowedScopes,
AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(redirectURI)},
TokenLifetimes: configv1alpha1.OIDCClientTokenLifetimes{IDTokenSeconds: tokenLifetimesIDTokenSeconds},
},
}
}
@@ -73,6 +75,7 @@ func FullyCapableOIDCClientAndStorageSecret(
clientID string,
clientUID string,
redirectURI string,
tokenLifetimesIDTokenSeconds *int32,
hashes []string,
validateFunc OIDCClientValidatorFunc,
) (*configv1alpha1.OIDCClient, *corev1.Secret) {
@@ -82,7 +85,7 @@ func FullyCapableOIDCClientAndStorageSecret(
"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token",
}
return OIDCClientAndStorageSecret(t, namespace, clientID, clientUID, allGrantTypes, allScopes, redirectURI, hashes, validateFunc)
return OIDCClientAndStorageSecret(t, namespace, clientID, clientUID, allGrantTypes, allScopes, redirectURI, tokenLifetimesIDTokenSeconds, hashes, validateFunc)
}
// OIDCClientAndStorageSecret returns an OIDC client which is allowed to use the specified grant types and scopes,
@@ -96,10 +99,11 @@ func OIDCClientAndStorageSecret(
allowedGrantTypes []configv1alpha1.GrantType,
allowedScopes []configv1alpha1.Scope,
redirectURI string,
tokenLifetimesIDTokenSeconds *int32,
hashes []string,
validateFunc OIDCClientValidatorFunc,
) (*configv1alpha1.OIDCClient, *corev1.Secret) {
oidcClient := newOIDCClient(namespace, clientID, clientUID, redirectURI, allowedGrantTypes, allowedScopes)
oidcClient := newOIDCClient(namespace, clientID, clientUID, redirectURI, allowedGrantTypes, allowedScopes, tokenLifetimesIDTokenSeconds)
secret := OIDCClientSecretStorageSecretForUID(t, namespace, clientUID, hashes)
// If a test made an invalid OIDCClient then inform the author of the test, so they can fix the test case.