mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-01-08 23:23:39 +00:00
Unit tests for token endpoint for custom ID token lifetimes
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
},
|
||||
|
||||
@@ -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))
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user