8000 Record existing AES key sizes to prevent key regeneration by lhanusov · Pull Request #54910 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,12 @@ Review and update their template overrides and CSS to match the new structure.
Notable changes may include internal behavior changes that prevent common misconfigurations, bugs that are fixed, or changes to simplify running {project_name}.
It also lists significant changes to internal APIs.

=== Existing AES key sizes are recorded during migration

When upgrading, existing `aes-generated` key providers without an explicit `secretSize` are updated to record the size of their stored key, including databases already running 26.8.x.
This ensures the Admin Console displays the actual key size and saving a provider without changes preserves its key and key ID.
Providers with an explicitly configured size are preserved.

=== SAML assertions with a OneTimeUse condition are no longer replayable
// https://github.com/keycloak/keycloak/issues/46325

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.keycloak.migration.migrators;

import org.keycloak.common.util.Base64Url;
import org.keycloak.migration.ModelVersion;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;

/**
* Records existing AES key sizes so saving a legacy provider preserves its key.
*/
public class MigrateTo26_8_3 extends RealmMigration {

public static final ModelVersion VERSION = new ModelVersion("26.8.3");

@Override
public ModelVersion getVersion() {
return VERSION;
}

@Override
public void migrateRealm(KeycloakSession session, RealmModel realm) {
realm.getComponentsStream(realm.getId(), "org.keycloak.keys.KeyProvider")
.filter(c -> "aes-generated".equals(c.getProviderId()))
.filter(c -> c.getConfig().getFirst("secretSize") == null)
.filter(c -> c.getConfig().getFirst("secret") != null)
.forEach(c -> {
// Preserve existing keys when the admin console saves the provider with the displayed size.
int size = Base64Url.decode(c.getConfig().getFirst("secret")).length;
c.getConfig().putSingle("secretSize", Integer.toString(size));
realm.updateComponent(c);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import org.keycloak.migration.migrators.MigrateTo26_7_0;
import org.keycloak.migration.migrators.MigrateTo26_8_0;
import org.keycloak.migration.migrators.MigrateTo26_8_2;
import org.keycloak.migration.migrators.MigrateTo26_8_3;
import org.keycloak.migration.migrators.MigrateTo27_0_0;
import org.keycloak.migration.migrators.MigrateTo2_0_0;
import org.keycloak.migration.migrators.MigrateTo2_1_0;
Expand Down Expand Up @@ -143,6 +144,7 @@ public class DefaultMigrationManager implements MigrationManager {
new MigrateTo26_7_0(),
new MigrateTo26_8_0(),
new MigrateTo26_8_2(),
new MigrateTo26_8_3(),
new MigrateTo27_0_0(),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
Expand All @@ -43,17 +44,22 @@
import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticatorFactory;
import org.keycloak.authentication.authenticators.conditional.ConditionalUserConfiguredAuthenticatorFactory;
import org.keycloak.common.constants.KerberosConstants;
import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.PrioritizedComponentModel;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.crypto.Algorithm;
import org.keycloak.keys.KeyProvider;
import org.keycloak.migration.migrators.MigrateTo26_8_3;
import org.keycloak.models.AccountRoles;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.Constants;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.UserModel;
import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.jpa.entities.ComponentEntity;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
Expand Down Expand Up @@ -454,6 +460,45 @@ protected void testMigrationTo26_4_0() {
testSamlEncryptionAttributes(migrationRealm);
}

protected void testMigrationTo26_8_3(RunOnServerClient runOnServer) {
List<ComponentRepresentation> providers = migrationRealm.components()
.query(migrationRealm.toRepresentation().getId(), KeyProvider.class.getName()).stream()
.filter(c -> "aes-generated".equals(c.getProviderId()))
.sorted(Comparator.comparing(ComponentRepresentation::getName)).toList();
assertThat(providers, hasSize(2));
for (ComponentRepresentation provider : providers) {
ComponentRepresentation imported = runOnServer.fetch(RunHelpers.internalComponent(provider.getId()));
// The fixture includes a key matching the current default size, which import validation does not backfill.
assertThat(provider.getConfig().get("secretSize"),
equalTo(List.of(Integer.toString(Base64Url.decode(imported.getConfig().getFirst("secret")).length))));

String componentId = provider.getId();
runOnServer.run(session -> {
// Recreate legacy database state without model validation filling the size back in.
ComponentEntity entity = session.getProvider(JpaConnectionProvider.class).getEntityManager()
.find(ComponentEntity.class, componentId);
entity.getComponentConfigs().removeIf(c -> "secretSize".equals(c.getName()));
session.getProvider(CacheRealmProvider.class).registerRealmInvalidation(
session.getContext().getRealm().getId(), session.getContext().getRealm().getName());
});
ComponentRepresentation before = runOnServer.fetch(RunHelpers.internalComponent(componentId));
assertNull(before.getConfig().getFirst("secretSize"));

runOnServer.run(session -> new MigrateTo26_8_3().migrateRealm(session, session.getContext().getRealm()));

ComponentRepresentation migrated = runOnServer.fetch(RunHelpers.internalComponent(componentId));
assertThat(migrated.getConfig(), equalTo(imported.getConfig()));
assertThat(migrated.getConfig().getFirst("secret"), equalTo(before.getConfig().getFirst("secret")));
assertThat(migrated.getConfig().getFirst("kid"), equalTo(before.getConfig().getFirst("kid")));

migrationRealm.components().component(componentId).update(provider);

ComponentRepresentation after = runOnServer.fetch(RunHelpers.internalComponent(componentId));
assertThat(after.getConfig().getFirst("secret"), equalTo(before.getConfig().getFirst("secret")));
assertThat(after.getConfig().getFirst("kid"), equalTo(before.getConfig().getFirst("kid")));
}
}

protected void testMigrationTo27_0_0(RealmResource realmWithUnsetTheme) {
Assertions.assertEquals(PREFERRED_DEFAULT_LOGIN_THEME, migrationRealm.toRepresentation().getLoginTheme(),
"Login v1 realm theme was not migrated to keycloak.v2");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
import org.keycloak.userprofile.config.UPConfigUtils;

import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -58,6 +60,9 @@ public class JsonFileImport1903MigrationTest extends AbstractJsonFileImportMigra
@InjectRealm(ref = "userProfile", fromJson = "migration-realm-19.0.3-user-profile.json")
ManagedRealm userProfileManagedRealm;

@InjectRunOnServer(realmRef = "migration")
RunOnServerClient migrationRunOnServer;

@Test
void migration19_0_3Test() {
checkRealmsImported();
Expand All @@ -69,6 +74,7 @@ void migration19_0_3Test() {
testMigrationTo25_0_0();
testMigrationTo26_0_0(true);
testMigrationTo26_3_0();
testMigrationTo26_8_3(migrationRunOnServer);
testMigrationTo27_0_0(userProfileManagedRealm.admin());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1632,6 +1632,23 @@
]
}
},
{
"id": "ff2c4bb4-563e-483b-87b9-a9c24594ea23",
"name": "aes-generated-256",
"providerId": "aes-generated",
"subComponents": {},
"config": {
"kid": [
"cb0a1c3d-902f-4e5d-905b-581bda97f090"
],
"secret": [
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
],
"priority": [
"-100"
]
}
},
{
"id": "51cbfea7-ca8b-479d-aa9c-9df7df9ccb26",
"name": "rsa-enc-generated",
Expand Down Expand Up @@ -2365,4 +2382,4 @@
"clientPolicies": {
"policies": []
}
}
}
Loading
0