fix(oidc): enable webkey feature by default (#10683)

# Which Problems Are Solved

When the webkey feature flag was not enabled before an upgrade to v4,
all JWT tokens became invalid.
This created a couple of issues:

- All users with JWT access tokens are logged-out
- Clients that are unable to refresh keys based on key ID break
- id_token_hint could no longer be validated.

# How the Problems Are Solved

Force-enable the webkey feature on the v3 version, so that the upgrade
path is cleaner. Sessions now have time to role-over to the new keys
before initiating the upgrade to v4.

# Additional Changes

- none

# Additional Context

- Related https://github.com/zitadel/zitadel/issues/10673

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Tim Möhlmann
2025-09-10 08:53:29 +03:00
committed by GitHub
parent 330928f8b5
commit ca510c52dd
4 changed files with 26 additions and 23 deletions

View File

@@ -1116,7 +1116,7 @@ DefaultInstance:
# - 3 # Project
# - 4 # UserGrant
# - 5 # OrgDomainVerified
# WebKey: false # ZITADEL_DEFAULTINSTANCE_FEATURES_WEBKEY
WebKey: true # ZITADEL_DEFAULTINSTANCE_FEATURES_WEBKEY
# DebugOIDCParentError: false # ZITADEL_DEFAULTINSTANCE_FEATURES_DEBUGOIDCPARENTERROR
# OIDCSingleV1SessionTermination: false # ZITADEL_DEFAULTINSTANCE_FEATURES_OIDCSINGLEV1SESSIONTERMINATION
# DisableUserTokenEvent: false # ZITADEL_DEFAULTINSTANCE_FEATURES_DISABLEUSERTOKENEVENT

View File

@@ -4,11 +4,11 @@ import (
"context"
"fmt"
"github.com/muhlemmer/gu"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/instance"
)
@@ -34,21 +34,20 @@ func (mig *SetupWebkeys) Execute(ctx context.Context, _ eventstore.Event) error
if err != nil {
return fmt.Errorf("%s get instance IDs: %w", mig, err)
}
conf := &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits2048,
Hasher: crypto.RSAHasherSHA256,
}
for _, instance := range instances {
ctx := authz.WithInstanceID(ctx, instance)
logging.Info("prepare initial webkeys for instance", "instance_id", instance, "migration", mig)
if err := mig.commands.GenerateInitialWebKeys(ctx, conf); err != nil {
return fmt.Errorf("%s generate initial webkeys: %w", mig, err)
_, err := mig.commands.SetInstanceFeatures(ctx, &command.InstanceFeatures{
WebKey: gu.Ptr(true),
})
if err != nil {
return fmt.Errorf("%s set webkey instance feature: %w", mig, err)
}
}
return nil
}
func (mig *SetupWebkeys) String() string {
return "59_setup_webkeys"
return "59_setup_webkeys_2"
}

View File

@@ -34,7 +34,7 @@ func TestMain(m *testing.M) {
}
func TestServer_Feature_Disabled(t *testing.T) {
instance, iamCtx, _ := createInstance(t, false)
instance, iamCtx, _ := createInstance(t, true)
client := instance.Client.WebKeyV2Beta
t.Run("CreateWebKey", func(t *testing.T) {
@@ -60,7 +60,7 @@ func TestServer_Feature_Disabled(t *testing.T) {
}
func TestServer_ListWebKeys(t *testing.T) {
instance, iamCtx, creationDate := createInstance(t, true)
instance, iamCtx, creationDate := createInstance(t, false)
// After the feature is first enabled, we can expect 2 generated keys with the default config.
checkWebKeyListState(iamCtx, t, instance, 2, "", &webkey.WebKey_Rsa{
Rsa: &webkey.RSA{
@@ -71,7 +71,7 @@ func TestServer_ListWebKeys(t *testing.T) {
}
func TestServer_CreateWebKey(t *testing.T) {
instance, iamCtx, creationDate := createInstance(t, true)
instance, iamCtx, creationDate := createInstance(t, false)
client := instance.Client.WebKeyV2Beta
_, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{
@@ -93,7 +93,7 @@ func TestServer_CreateWebKey(t *testing.T) {
}
func TestServer_ActivateWebKey(t *testing.T) {
instance, iamCtx, creationDate := createInstance(t, true)
instance, iamCtx, creationDate := createInstance(t, false)
client := instance.Client.WebKeyV2Beta
resp, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{
@@ -120,7 +120,7 @@ func TestServer_ActivateWebKey(t *testing.T) {
}
func TestServer_DeleteWebKey(t *testing.T) {
instance, iamCtx, creationDate := createInstance(t, true)
instance, iamCtx, creationDate := createInstance(t, false)
client := instance.Client.WebKeyV2Beta
keyIDs := make([]string, 2)
@@ -197,14 +197,14 @@ func TestServer_DeleteWebKey(t *testing.T) {
}, creationDate)
}
func createInstance(t *testing.T, enableFeature bool) (*integration.Instance, context.Context, *timestamppb.Timestamp) {
func createInstance(t *testing.T, disableFeature bool) (*integration.Instance, context.Context, *timestamppb.Timestamp) {
instance := integration.NewInstance(CTX)
creationDate := timestamppb.Now()
creationDate := instance.Instance.GetDetails().GetCreationDate()
iamCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
if enableFeature {
if disableFeature {
_, err := instance.Client.FeatureV2.SetInstanceFeatures(iamCTX, &feature.SetInstanceFeaturesRequest{
WebKey: proto.Bool(true),
WebKey: proto.Bool(false),
})
require.NoError(t, err)
}
@@ -212,11 +212,11 @@ func createInstance(t *testing.T, enableFeature bool) (*integration.Instance, co
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamCTX, time.Minute)
assert.EventuallyWithT(t, func(collect *assert.CollectT) {
resp, err := instance.Client.WebKeyV2Beta.ListWebKeys(iamCTX, &webkey.ListWebKeysRequest{})
if enableFeature {
if disableFeature {
assert.Error(collect, err)
} else {
assert.NoError(collect, err)
assert.Len(collect, resp.GetWebKeys(), 2)
} else {
assert.Error(collect, err)
}
}, retryDuration, tick)
@@ -244,8 +244,8 @@ func checkWebKeyListState(ctx context.Context, t *testing.T, instance *integrati
now := time.Now()
var gotActiveKeyID string
for _, key := range list {
assert.WithinRange(collect, key.GetCreationDate().AsTime(), now.Add(-time.Minute), now.Add(time.Minute))
assert.WithinRange(collect, key.GetChangeDate().AsTime(), now.Add(-time.Minute), now.Add(time.Minute))
assert.WithinRange(collect, key.GetCreationDate().AsTime(), creationDate.AsTime(), now.Add(time.Minute))
assert.WithinRange(collect, key.GetChangeDate().AsTime(), creationDate.AsTime(), now.Add(time.Minute))
assert.NotEqual(collect, webkey.State_STATE_UNSPECIFIED, key.GetState())
assert.NotEqual(collect, webkey.State_STATE_REMOVED, key.GetState())
assert.Equal(collect, config, key.GetKey())

View File

@@ -25,6 +25,10 @@ import (
func TestServer_Keys(t *testing.T) {
instance := integration.NewInstance(CTX)
// As we want to test the legacy keys as well, we need to ensure the webkey feature is off
// at the beginning since the instance creation enables it by default.
ensureWebKeyFeature(t, instance, false)
ctxLogin := instance.WithAuthorization(CTX, integration.UserTypeLogin)
clientID, _ := createClient(t, instance)