Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions cmd/server/serve-cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,15 +257,15 @@ const (

func createRedisClients(cli *cli.Context, logger *slog.Logger) (*service.RedisConfig, error) {

token, _, err := createRedisClient(cli, logger, redisDatabaseTokens)
_, token, err := createRedisClient(cli, logger, redisDatabaseTokens)
if err != nil {
return nil, err
}
rate, _, err := createRedisClient(cli, logger, redisDatabaseRateLimiting)
_, rate, err := createRedisClient(cli, logger, redisDatabaseRateLimiting)
if err != nil {
return nil, err
}
invite, _, err := createRedisClient(cli, logger, redisDatabaseInvites)
_, invite, err := createRedisClient(cli, logger, redisDatabaseInvites)
if err != nil {
return nil, err
}
Expand Down
7 changes: 3 additions & 4 deletions cmd/server/token-cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,14 @@ func newTokenCmd() *cli.Command {
return fmt.Errorf("unable to create logger %w", err)
}

tokenRedisClient, _, err := createRedisClient(ctx, log, redisDatabaseTokens)
_, tokenValkeyClient, err := createRedisClient(ctx, log, redisDatabaseTokens)
if err != nil {
return err
}

tokenStore := tokencommon.NewRedisStore(tokenRedisClient)
tokenStore := tokencommon.NewRedisStore(tokenValkeyClient)
certStore := certs.NewRedisStore(&certs.Config{
RedisClient: tokenRedisClient,
ValkeyClient: tokenValkeyClient,
})

tokenService := token.New(token.Config{
Expand Down Expand Up @@ -136,7 +136,6 @@ func newTokenCmd() *cli.Command {

var adminRole *apiv2.AdminRole
if roleString := ctx.String(tokenAdminRoleFlag.Name); roleString != "" {
// FIXME new linter complains that role is never used
role, ok := apiv2.AdminRole_value[roleString]
if !ok {
return fmt.Errorf("unknown role: %s", roleString)
Expand Down
22 changes: 9 additions & 13 deletions pkg/auth/authenticator-interceptor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,44 +10,45 @@ import (
"testing"
"time"

"github.com/alicebob/miniredis/v2"
"github.com/golang-jwt/jwt/v5"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
v2 "github.com/metal-stack/api/go/metalstack/api/v2"
"github.com/metal-stack/metal-apiserver/pkg/certs"
"github.com/metal-stack/metal-apiserver/pkg/errorutil"
"github.com/metal-stack/metal-apiserver/pkg/repository/api"
"github.com/metal-stack/metal-apiserver/pkg/test"
"github.com/metal-stack/metal-apiserver/pkg/token"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/require"
"github.com/valkey-io/valkey-go"
)

func prepare(t *testing.T) (certs.CertStore, *ecdsa.PrivateKey) {
s := miniredis.RunT(t)
c := redis.NewClient(&redis.Options{Addr: s.Addr()})
func prepare(t *testing.T) (valkey.Client, certs.CertStore, *ecdsa.PrivateKey) {
_, c, _ := test.StartValkey(t, test.WithMiniRedis(true))

// creating an initial signing certificate
store := certs.NewRedisStore(&certs.Config{
RedisClient: c,
ValkeyClient: c,
})
_, err := store.LatestPrivate(t.Context())
require.NoError(t, err)

key, err := store.LatestPrivate(t.Context())
require.NoError(t, err)

return store, key
return c, store, key
}

func Test_authorize_with_permissions(t *testing.T) {
pk, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
require.NoError(t, err)
var (
expired = -time.Hour
certStore, key = prepare(t)
c, certStore, key = prepare(t)
defaultIssuer = "https://api-server"
maliciousSigningKey = pk
tokenStore = token.NewRedisStore(c)
ctx = t.Context()
)

tests := []struct {
Expand Down Expand Up @@ -152,11 +153,6 @@ func Test_authorize_with_permissions(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := miniredis.RunT(t)
defer s.Close()

ctx := t.Context()
tokenStore := token.NewRedisStore(redis.NewClient(&redis.Options{Addr: s.Addr()}))

exp := time.Hour
if tt.expiration != nil {
Expand Down
201 changes: 102 additions & 99 deletions pkg/auth/jwt-token-cert-rotation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,19 @@ import (
"fmt"
"log/slog"
"testing"
"testing/synctest"
"time"

"github.com/alicebob/miniredis/v2"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/metal-stack/metal-apiserver/pkg/certs"
tokenservice "github.com/metal-stack/metal-apiserver/pkg/service/api/token"
"github.com/metal-stack/metal-apiserver/pkg/token"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/require"
"github.com/valkey-io/valkey-go"
)

func Test_jwt_cert_rotation(t *testing.T) {
t.Parallel()
// t.Parallel()
Comment on lines -21 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either remove or comment in

oldMaxExpiration := token.MaxExpiration
oldDefaultExpiration := token.DefaultExpiration

Expand All @@ -33,11 +32,15 @@ func Test_jwt_cert_rotation(t *testing.T) {
t.Logf("token lifetime: %s, certificate lifetime: %s, issue new signing certificate after: %s", token.DefaultExpiration, 2*token.MaxExpiration, 2*token.MaxExpiration-renewCertBeforeExpiration)

s := miniredis.RunT(t)
c := redis.NewClient(&redis.Options{Addr: s.Addr()})
c, err := valkey.NewClient(valkey.ClientOption{
InitAddress: []string{s.Addr()},
DisableCache: true,
})
require.NoError(t, err)
log := slog.Default()

certStore := certs.NewRedisStore(&certs.Config{
RedisClient: c,
ValkeyClient: c,
RenewCertBeforeExpiration: &renewCertBeforeExpiration,
})
tokenStore := token.NewRedisStore(c)
Expand Down Expand Up @@ -66,111 +69,111 @@ func Test_jwt_cert_rotation(t *testing.T) {
return s
}()

synctest.Test(t, func(t *testing.T) {

ctx := t.Context()

var (
token1 = ""
token2 = ""
token3 = ""
previousAt *time.Duration
)
steps := []struct {
name string
at time.Duration
task func(t *testing.T)
}{
{
name: "token 1",
at: 0 * time.Second,
task: func(t *testing.T) {
token1 = createNewConsoleToken(t, ctx, service)
expectCertStore(t, ctx, certStore, 1)
expectTokenWorks(t, ctx, auth, token1)
},
// synctest.Test(t, func(t *testing.T) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

valkey-go has its own background worker which breaks synctest.Test


ctx := t.Context()

var (
token1 = ""
token2 = ""
token3 = ""
previousAt *time.Duration
)
steps := []struct {
name string
at time.Duration
task func(t *testing.T)
}{
{
name: "token 1",
at: 0 * time.Second,
task: func(t *testing.T) {
token1 = createNewConsoleToken(t, ctx, service)
expectCertStore(t, ctx, certStore, 1)
expectTokenWorks(t, ctx, auth, token1)
},
{
name: "token2",
at: 2 * time.Second,
task: func(t *testing.T) {
token2 = createNewConsoleToken(t, ctx, service)
expectCertStore(t, ctx, certStore, 1)
expectTokenWorks(t, ctx, auth, token1)
expectTokenWorks(t, ctx, auth, token2)
},
},
{
name: "token2",
at: 2 * time.Second,
task: func(t *testing.T) {
token2 = createNewConsoleToken(t, ctx, service)
expectCertStore(t, ctx, certStore, 1)
expectTokenWorks(t, ctx, auth, token1)
expectTokenWorks(t, ctx, auth, token2)
},
{
name: "token3, next signing cert gets created",
at: 4 * time.Second,
task: func(t *testing.T) {
token3 = createNewConsoleToken(t, ctx, service)
expectCertStore(t, ctx, certStore, 2)
expectTokenWorks(t, ctx, auth, token1)
expectTokenWorks(t, ctx, auth, token2)
expectTokenWorks(t, ctx, auth, token3)
},
},
{
name: "token3, next signing cert gets created",
at: 4 * time.Second,
task: func(t *testing.T) {
token3 = createNewConsoleToken(t, ctx, service)
expectCertStore(t, ctx, certStore, 2)
expectTokenWorks(t, ctx, auth, token1)
expectTokenWorks(t, ctx, auth, token2)
expectTokenWorks(t, ctx, auth, token3)
},
{
name: "token1 expired, token 2 and 3 still work",
at: 6 * time.Second,
task: func(t *testing.T) {
token3 = createNewConsoleToken(t, ctx, service)
expectCertStore(t, ctx, certStore, 2)
expectTokenExpired(t, ctx, auth, token1)
expectTokenWorks(t, ctx, auth, token2)
expectTokenWorks(t, ctx, auth, token3)
},
},
{
name: "token1 expired, token 2 and 3 still work",
at: 6 * time.Second,
task: func(t *testing.T) {
token3 = createNewConsoleToken(t, ctx, service)
expectCertStore(t, ctx, certStore, 2)
expectTokenExpired(t, ctx, auth, token1)
expectTokenWorks(t, ctx, auth, token2)
expectTokenWorks(t, ctx, auth, token3)
},
{
name: "token1 and token2 expired, token 3 still works",
at: 8 * time.Second,
task: func(t *testing.T) {
expectCertStore(t, ctx, certStore, 2)
expectTokenExpired(t, ctx, auth, token1)
expectTokenExpired(t, ctx, auth, token2)
expectTokenWorks(t, ctx, auth, token3)
},
},
{
name: "token1 and token2 expired, token 3 still works",
at: 8 * time.Second,
task: func(t *testing.T) {
expectCertStore(t, ctx, certStore, 2)
expectTokenExpired(t, ctx, auth, token1)
expectTokenExpired(t, ctx, auth, token2)
expectTokenWorks(t, ctx, auth, token3)
},
{
name: "all tokens expired, first signing cert is gone",
at: 11 * time.Second,
task: func(t *testing.T) {
expectCertStore(t, ctx, certStore, 1)
expectTokenNoPublicKeyForSignatureFound(t, ctx, auth, token1)
expectTokenNoPublicKeyForSignatureFound(t, ctx, auth, token2)
expectTokenExpired(t, ctx, auth, token3)
},
},
{
name: "all tokens expired, first signing cert is gone",
at: 11 * time.Second,
task: func(t *testing.T) {
expectCertStore(t, ctx, certStore, 1)
expectTokenNoPublicKeyForSignatureFound(t, ctx, auth, token1)
expectTokenNoPublicKeyForSignatureFound(t, ctx, auth, token2)
expectTokenExpired(t, ctx, auth, token3)
},
{
name: "all tokens expired, all signing certs gone",
at: 15 * time.Second,
task: func(t *testing.T) {
expectCertStore(t, ctx, certStore, 0)
expectTokenNoPublicKeyForSignatureFound(t, ctx, auth, token1)
expectTokenNoPublicKeyForSignatureFound(t, ctx, auth, token2)
expectTokenNoPublicKeyForSignatureFound(t, ctx, auth, token3)
},
},
{
name: "all tokens expired, all signing certs gone",
at: 15 * time.Second,
task: func(t *testing.T) {
expectCertStore(t, ctx, certStore, 0)
expectTokenNoPublicKeyForSignatureFound(t, ctx, auth, token1)
expectTokenNoPublicKeyForSignatureFound(t, ctx, auth, token2)
expectTokenNoPublicKeyForSignatureFound(t, ctx, auth, token3)
},
}
},
}

time.Sleep(1 * time.Second)
time.Sleep(1 * time.Second)

for _, step := range steps {
forwardText := ""
if previousAt != nil {
forward := step.at - *previousAt
forwardText = fmt.Sprintf(" (forwarding by %s)", forward)
time.Sleep(forward)
s.FastForward(forward)
}
previousAt = &step.at
for _, step := range steps {
forwardText := ""
if previousAt != nil {
forward := step.at - *previousAt
forwardText = fmt.Sprintf(" (forwarding by %s)", forward)
time.Sleep(forward)
s.FastForward(forward)
}
previousAt = &step.at

t.Logf("%s: running step at %q%s: %q", time.Now(), step.at, forwardText, step.name)
t.Logf("%s: running step at %q%s: %q", time.Now(), step.at, forwardText, step.name)

step.task(t)
}
})
step.task(t)
}
// })
}

func createNewConsoleToken(t *testing.T, ctx context.Context, service tokenservice.TokenService) string {
Expand Down
Loading