Merge pull request #614 from vmware-tanzu/gc-bug-tests

Tests for garbage collection behavior for access and refresh tokens
This commit is contained in:
Margo Crawford 2021-05-13 13:08:07 -07:00 committed by GitHub
commit 39d7f8b6eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 80 additions and 5 deletions

View File

@ -9,8 +9,10 @@ import (
"crypto/elliptic" "crypto/elliptic"
"crypto/rand" "crypto/rand"
"crypto/sha256" "crypto/sha256"
"encoding/base32"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -575,7 +577,7 @@ func TestTokenEndpointWhenAuthcodeIsUsedTwice(t *testing.T) {
require.JSONEq(t, fositeReusedAuthCodeErrorBody, reusedAuthcodeResponse.Body.String()) require.JSONEq(t, fositeReusedAuthCodeErrorBody, reusedAuthcodeResponse.Body.String())
// This was previously invalidated by the first request, so it remains invalidated // This was previously invalidated by the first request, so it remains invalidated
requireInvalidAuthCodeStorage(t, authCode, oauthStore) requireInvalidAuthCodeStorage(t, authCode, oauthStore, secrets)
// Has now invalidated the access token that was previously handed out by the first request // Has now invalidated the access token that was previously handed out by the first request
requireInvalidAccessTokenStorage(t, parsedResponseBody, oauthStore) requireInvalidAccessTokenStorage(t, parsedResponseBody, oauthStore)
// This was previously invalidated by the first request, so it remains invalidated // This was previously invalidated by the first request, so it remains invalidated
@ -1200,8 +1202,8 @@ func requireTokenEndpointBehavior(
wantIDToken := contains(test.wantSuccessBodyFields, "id_token") wantIDToken := contains(test.wantSuccessBodyFields, "id_token")
wantRefreshToken := contains(test.wantSuccessBodyFields, "refresh_token") wantRefreshToken := contains(test.wantSuccessBodyFields, "refresh_token")
requireInvalidAuthCodeStorage(t, authCode, oauthStore) requireInvalidAuthCodeStorage(t, authCode, oauthStore, secrets)
requireValidAccessTokenStorage(t, parsedResponseBody, oauthStore, test.wantRequestedScopes, test.wantGrantedScopes) requireValidAccessTokenStorage(t, parsedResponseBody, oauthStore, test.wantRequestedScopes, test.wantGrantedScopes, secrets)
requireInvalidPKCEStorage(t, authCode, oauthStore) requireInvalidPKCEStorage(t, authCode, oauthStore)
requireValidOIDCStorage(t, parsedResponseBody, authCode, oauthStore, test.wantRequestedScopes, test.wantGrantedScopes) requireValidOIDCStorage(t, parsedResponseBody, authCode, oauthStore, test.wantRequestedScopes, test.wantGrantedScopes)
@ -1215,7 +1217,7 @@ func requireTokenEndpointBehavior(
requireValidIDToken(t, parsedResponseBody, jwtSigningKey, wantAtHashClaimInIDToken, wantNonceValueInIDToken, parsedResponseBody["access_token"].(string)) requireValidIDToken(t, parsedResponseBody, jwtSigningKey, wantAtHashClaimInIDToken, wantNonceValueInIDToken, parsedResponseBody["access_token"].(string))
} }
if wantRefreshToken { if wantRefreshToken {
requireValidRefreshTokenStorage(t, parsedResponseBody, oauthStore, test.wantRequestedScopes, test.wantGrantedScopes) requireValidRefreshTokenStorage(t, parsedResponseBody, oauthStore, test.wantRequestedScopes, test.wantGrantedScopes, secrets)
} }
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1) testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1)
@ -1436,12 +1438,15 @@ func requireInvalidAuthCodeStorage(
t *testing.T, t *testing.T,
code string, code string,
storage oauth2.CoreStorage, storage oauth2.CoreStorage,
secrets v1.SecretInterface,
) { ) {
t.Helper() t.Helper()
// Make sure we have invalidated this auth code. // Make sure we have invalidated this auth code.
_, err := storage.GetAuthorizeCodeSession(context.Background(), getFositeDataSignature(t, code), nil) _, err := storage.GetAuthorizeCodeSession(context.Background(), getFositeDataSignature(t, code), nil)
require.True(t, errors.Is(err, fosite.ErrInvalidatedAuthorizeCode)) require.True(t, errors.Is(err, fosite.ErrInvalidatedAuthorizeCode))
// make sure that its still around in storage so if someone tries to use it again we invalidate everything
requireGarbageCollectTimeInDelta(t, code, "authcode", secrets, time.Now().Add(9*time.Hour).Add(10*time.Minute), 30*time.Second)
} }
func requireValidRefreshTokenStorage( func requireValidRefreshTokenStorage(
@ -1450,6 +1455,7 @@ func requireValidRefreshTokenStorage(
storage oauth2.CoreStorage, storage oauth2.CoreStorage,
wantRequestedScopes []string, wantRequestedScopes []string,
wantGrantedScopes []string, wantGrantedScopes []string,
secrets v1.SecretInterface,
) { ) {
t.Helper() t.Helper()
@ -1471,6 +1477,8 @@ func requireValidRefreshTokenStorage(
wantGrantedScopes, wantGrantedScopes,
true, true,
) )
requireGarbageCollectTimeInDelta(t, refreshTokenString, "refresh-token", secrets, time.Now().Add(9*time.Hour).Add(2*time.Minute), 1*time.Minute)
} }
func requireValidAccessTokenStorage( func requireValidAccessTokenStorage(
@ -1479,6 +1487,7 @@ func requireValidAccessTokenStorage(
storage oauth2.CoreStorage, storage oauth2.CoreStorage,
wantRequestedScopes []string, wantRequestedScopes []string,
wantGrantedScopes []string, wantGrantedScopes []string,
secrets v1.SecretInterface,
) { ) {
t.Helper() t.Helper()
@ -1519,6 +1528,8 @@ func requireValidAccessTokenStorage(
wantGrantedScopes, wantGrantedScopes,
true, true,
) )
requireGarbageCollectTimeInDelta(t, accessTokenString, "access-token", secrets, time.Now().Add(9*time.Hour).Add(2*time.Minute), 1*time.Minute)
} }
func requireInvalidAccessTokenStorage( func requireInvalidAccessTokenStorage(
@ -1681,6 +1692,24 @@ func requireValidStoredRequest(
require.Empty(t, session.Subject) require.Empty(t, session.Subject)
} }
func requireGarbageCollectTimeInDelta(t *testing.T, tokenString string, typeLabel string, secrets v1.SecretInterface, wantExpirationTime time.Time, deltaTime time.Duration) {
t.Helper()
signature := getFositeDataSignature(t, tokenString)
signatureBytes, err := base64.RawURLEncoding.DecodeString(signature)
require.NoError(t, err)
// lower case base32 encoding insures that our secret name is valid per ValidateSecretName in k/k
var b32 = base32.StdEncoding.WithPadding(base32.NoPadding)
signatureAsValidName := strings.ToLower(b32.EncodeToString(signatureBytes))
secretName := fmt.Sprintf("pinniped-storage-%s-%s", typeLabel, signatureAsValidName)
secret, err := secrets.Get(context.Background(), secretName, metav1.GetOptions{})
require.NoError(t, err)
refreshTokenGCTimeString := secret.Annotations["storage.pinniped.dev/garbage-collect-after"]
refreshTokenGCTime, err := time.Parse(crud.SecretLifetimeAnnotationDateFormat, refreshTokenGCTimeString)
require.NoError(t, err)
testutil.RequireTimeInDelta(t, refreshTokenGCTime, wantExpirationTime, deltaTime)
}
func requireValidIDToken( func requireValidIDToken(
t *testing.T, t *testing.T,
body map[string]interface{}, body map[string]interface{},

View File

@ -6,6 +6,7 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"context" "context"
"encoding/base32"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
@ -25,11 +26,13 @@ import (
authorizationv1 "k8s.io/api/authorization/v1" authorizationv1 "k8s.io/api/authorization/v1"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1" rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
authv1alpha "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" authv1alpha "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
"go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/certauthority"
"go.pinniped.dev/internal/crud"
"go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc"
"go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil"
"go.pinniped.dev/pkg/oidcclient" "go.pinniped.dev/pkg/oidcclient"
@ -42,7 +45,7 @@ import (
func TestE2EFullIntegration(t *testing.T) { func TestE2EFullIntegration(t *testing.T) {
env := library.IntegrationEnv(t) env := library.IntegrationEnv(t)
ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Minute) ctx, cancelFunc := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancelFunc() defer cancelFunc()
// Build pinniped CLI. // Build pinniped CLI.
@ -284,6 +287,38 @@ func TestE2EFullIntegration(t *testing.T) {
}) })
require.NotNil(t, token) require.NotNil(t, token)
// check that the access token is new (since it's just been refreshed) and has close to two minutes left.
testutil.RequireTimeInDelta(t, start.Add(2*time.Minute), token.AccessToken.Expiry.Time, 15*time.Second)
kubeClient := library.NewKubernetesClientset(t).CoreV1()
// get the access token secret that matches the signature from the cache
accessTokenSignature := strings.Split(token.AccessToken.Token, ".")[1]
accessSecretName := getSecretNameFromSignature(t, accessTokenSignature, "access-token")
accessTokenSecret, err := kubeClient.Secrets(env.SupervisorNamespace).Get(ctx, accessSecretName, metav1.GetOptions{})
require.NoError(t, err)
// Check that the access token garbage-collect-after value is 9 hours from now
accessTokenGCTimeString := accessTokenSecret.Annotations["storage.pinniped.dev/garbage-collect-after"]
accessTokenGCTime, err := time.Parse(crud.SecretLifetimeAnnotationDateFormat, accessTokenGCTimeString)
require.NoError(t, err)
require.True(t, accessTokenGCTime.After(time.Now().Add(9*time.Hour)))
// get the refresh token secret that matches the signature from the cache
refreshTokenSignature := strings.Split(token.RefreshToken.Token, ".")[1]
refreshSecretName := getSecretNameFromSignature(t, refreshTokenSignature, "refresh-token")
refreshTokenSecret, err := kubeClient.Secrets(env.SupervisorNamespace).Get(ctx, refreshSecretName, metav1.GetOptions{})
require.NoError(t, err)
// Check that the refresh token garbage-collect-after value is 9 hours
refreshTokenGCTimeString := refreshTokenSecret.Annotations["storage.pinniped.dev/garbage-collect-after"]
refreshTokenGCTime, err := time.Parse(crud.SecretLifetimeAnnotationDateFormat, refreshTokenGCTimeString)
require.NoError(t, err)
require.True(t, refreshTokenGCTime.After(time.Now().Add(9*time.Hour)))
// the access token and the refresh token should be garbage collected at essentially the same time
testutil.RequireTimeInDelta(t, accessTokenGCTime, refreshTokenGCTime, 1*time.Minute)
idTokenClaims := token.IDToken.Claims idTokenClaims := token.IDToken.Claims
require.Equal(t, env.SupervisorTestUpstream.Username, idTokenClaims[oidc.DownstreamUsernameClaim]) require.Equal(t, env.SupervisorTestUpstream.Username, idTokenClaims[oidc.DownstreamUsernameClaim])
@ -338,3 +373,14 @@ status:
append(env.SupervisorTestUpstream.ExpectedGroups, "system:authenticated"), append(env.SupervisorTestUpstream.ExpectedGroups, "system:authenticated"),
) )
} }
func getSecretNameFromSignature(t *testing.T, signature string, typeLabel string) string {
t.Helper()
// try to decode base64 signatures to prevent double encoding of binary data
signatureBytes, err := base64.RawURLEncoding.DecodeString(signature)
require.NoError(t, err)
// lower case base32 encoding insures that our secret name is valid per ValidateSecretName in k/k
var b32 = base32.StdEncoding.WithPadding(base32.NoPadding)
signatureAsValidName := strings.ToLower(b32.EncodeToString(signatureBytes))
return fmt.Sprintf("pinniped-storage-%s-%s", typeLabel, signatureAsValidName)
}