Skip to content

Stop storing client private keys in the database - #51612

Merged
mposolda merged 5 commits into
keycloak:mainfrom
jimmychakkalakal:weakness/49239-Stop-storing-client-private-keys-in-database
Aug 17, 2026
Merged

mposolda merged 5 commits into
keycloak:mainfrom
jimmychakkalakal:weakness/49239-Stop-storing-client-private-keys-in-database

Conversation

@jimmychakkalakal

Copy link
Copy Markdown
Contributor

The generate endpoint stored client private keys as clear-text client attributes. The server never uses these keys for any cryptographic operation — it only needs the public key/certificate to verify signature. This change:

  • Stops persisting private keys in generate, upload, and SAML client creation endpoints
  • Strips private keys from getKeyInfo responses
  • Deprecates the generate, download, and generate-and-download endpoints
  • Updates admin UI to reflect that private keys are downloaded once and not stored
  • Updates documentation with guidance to generate keys externally and examples for converting keystores to JWK format for JWKS_URL usage

Closes #49239

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Stops storing client private keys server-side while deprecating server-managed key generation.

Changes:

  • Persists only public certificates/keys and sanitizes key-info responses.
  • Deprecates key generation and download APIs.
  • Updates admin UI, tests, installation templates, and documentation.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tests/base/.../CredentialsTest.java Updates certificate persistence tests.
services/.../CertificateInfoHelper.java Omits private keys from responses.
services/.../ClientAttributeCertificateResource.java Sanitizes storage and deprecates endpoints.
services/.../SamlProtocolFactory.java Stops storing generated SAML private keys.
services/.../KeycloakSamlSubsystemCliInstallation.java Updates missing-key guidance.
services/.../KeycloakSamlClientInstallation.java Updates installation placeholders.
js/libs/.../clients.ts Deprecates admin-client methods.
js/apps/.../SamlKeysDialog.tsx Downloads generated keys once.
js/apps/.../SamlKeys.tsx Refreshes public key information.
js/apps/.../messages_en.properties Clarifies private-key handling.
integration/.../ClientAttributeCertificateResource.java Deprecates Java admin APIs.
docs/.../proc-creating-saml-client.adoc Documents SAML key ownership.
docs/.../con-confidential-client-credentials.adoc Adds external generation and JWKS guidance.
Suppressed comments (4)

services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java:186

  • Although the client attributes receive infoForStorage, the original upload representation (including an extracted private key) is still written to the admin event at line 188. Use the sanitized representation for the event as well so keystore uploads do not persist private keys in event storage.
            CertificateRepresentation infoForStorage = new CertificateRepresentation();
            infoForStorage.setCertificate(info.getCertificate());
            infoForStorage.setPublicKey(info.getPublicKey());
            infoForStorage.setKid(info.getKid());
            CertificateInfoHelper.updateClientModelCertificateInfo(client, infoForStorage, attributePrefix);

js/libs/keycloak-admin-client/src/resources/clients.ts:1015

  • The newly deprecated method is still called without suppression in ExportSamlKeyDialog.tsx:36 and clients.spec.ts:980. With the workspace's no-deprecated rule, those callers cause lint errors.
  /**
   * @deprecated Private keys are no longer stored on the server. The downloaded
   *             keystore will only contain the certificate.
   */

js/libs/keycloak-admin-client/src/resources/clients.ts:1032

  • Keys.tsx:87 and clients.spec.ts:955 still call this newly deprecated method without suppressing @typescript-eslint/no-deprecated, so the strict TypeScript lint configuration will reject them.
  /**
   * @deprecated Clients should generate their own key pairs and upload only the
   *             public key/certificate.
   */

tests/base/src/test/java/org/keycloak/tests/admin/client/CredentialsTest.java:147

  • getKeyInfo() now strips private keys independently of storage, so this assertion would pass even if generate() still persisted the private-key attribute. Verify the underlying client model/database attribute directly to cover the security requirement.
        // But server should not have stored it
        CertificateRepresentation certFromGet = certRsc.getKeyInfo();
        assertNull(certFromGet.getPrivateKey(), "private key should not be stored on server");

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@keycloak-github-bot keycloak-github-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unreported flaky test detected, please review

@keycloak-github-bot

Copy link
Copy Markdown

Unreported flaky test detected

If the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR.

org.keycloak.testsuite.webauthn.WebAuthnPropertyTest#timeout

Keycloak CI - WebAuthn IT

org.openqa.selenium.WebDriverException: 
unknown error: unhandled inspector error: {"code":-32000,"message":"Node with given id does not belong to the document"}
  (Session info: chrome=150.0.7871.128)
Build info: version: '4.28.1', revision: '73f5ad48a2'
System info: os.name: 'Linux', os.arch: 'amd64', os.version: '6.17.0-1020-azure', java.version: '25.0.3'
...

Report flaky test

@jimmychakkalakal
jimmychakkalakal force-pushed the weakness/49239-Stop-storing-client-private-keys-in-database branch from 5af0811 to 755617e Compare August 11, 2026 10:44
@jimmychakkalakal
jimmychakkalakal requested a balanced review from Copilot August 11, 2026 10:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.

@jimmychakkalakal
jimmychakkalakal force-pushed the weakness/49239-Stop-storing-client-private-keys-in-database branch from 755617e to 9237ba5 Compare August 11, 2026 13:34
@jimmychakkalakal
jimmychakkalakal requested a balanced review from Copilot August 11, 2026 13:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.

Suppressed comments (4)

services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java:204

  • This Javadoc still says the download contains a private key, contradicting the newly updated operation description and the implementation, which now obtains a public-only representation. Update it to state that the keystore contains only the public certificate.
     * @deprecated Clients should manage their own private keys.

js/apps/admin-ui/src/clients/keys/ExportSamlKeyDialog.tsx:37

  • This action is now misleading because downloadKey returns a certificate-only keystore, while the dialog is still titled “Export SAML Keys” and KeyForm requires a key password described as the private-key password. Operators may mistake this export for a private-key backup; rename it as certificate export and remove the key-password field, or remove the deprecated action.
      // eslint-disable-next-line @typescript-eslint/no-deprecated
      const keyStore = await adminClient.clients.downloadKey(

integration/admin-client-core/src/main/java/org/keycloak/admin/client/resource/ClientAttributeCertificateResource.java:90

  • The new deprecation text conflicts with the method description above, which still promises a keystore containing the private key. Update the public admin-client Javadoc to state that only the certificate is returned so callers do not treat this endpoint as private-key recovery.
     * @deprecated Clients should manage their own private keys.

services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java:173

  • The updated JWKS upload test uses a public-only fixture, so it does not exercise the security behavior added here. Add coverage with private RSA/EC (and ideally symmetric/OKP) parameters that verifies both the stored JWKS and getKeyInfo() output omit every entry in this set.
    private static final Set<String> JWK_PRIVATE_KEY_PARAMS = Set.of("d", "p", "q", "dp", "dq", "qi", "oth", "k");

    public static String stripPrivateKeyParams(String jwks) {

@jimmychakkalakal
jimmychakkalakal force-pushed the weakness/49239-Stop-storing-client-private-keys-in-database branch 3 times, most recently from 36fc63c to 180ce8c Compare August 12, 2026 13:37
@jimmychakkalakal
jimmychakkalakal marked this pull request as ready for review August 12, 2026 17:32
@jimmychakkalakal
jimmychakkalakal requested a review from a team as a code owner August 12, 2026 17:32
Copilot AI review requested due to automatic review settings August 12, 2026 17:32
@jimmychakkalakal
jimmychakkalakal requested review from a team as code owners August 12, 2026 17:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.

Suppressed comments (2)

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AbstractClientAuthSignedJWTTest.java:557

  • This assertion verifies equivalence after re-serialization, but it doesn’t validate the new security behavior (that private JWK parameters are removed before storing). Add an assertion here that the stored JWKS_STRING (or the parsed actual keys) does not contain private parameters like d/p/q/dp/dq/qi/oth/k, so the test directly covers the private-key stripping requirement.
            JSONWebKeySet expected = JsonSerialization.readValue(jwks, JSONWebKeySet.class);
            JSONWebKeySet actual = JsonSerialization.readValue(client.getAttributes().get(OIDCConfigAttributes.JWKS_STRING), JSONWebKeySet.class);
            Assertions.assertEquals(JsonSerialization.writeValueAsString(expected), JsonSerialization.writeValueAsString(actual));

tests/base/src/test/java/org/keycloak/tests/admin/client/CredentialsTest.java:156

  • adminEvents.poll() can return null, which would cause a NullPointerException at event.getRepresentation() and produce a less actionable test failure. Add an explicit assertNotNull(event, ...) (and potentially assertNotNull(event.getRepresentation(), ...)) before accessing it so failures clearly indicate the missing admin event.
        AdminEventRepresentation event = adminEvents.poll();
        assertFalse(event.getRepresentation().contains(cert.getPrivateKey()), "admin event should not contain private key");
        CertificateRepresentation expectedEventRep = new CertificateRepresentation();
        expectedEventRep.setCertificate(cert.getCertificate());
        AdminEventAssertion.assertEvent(event, OperationType.ACTION, AdminEventPaths.clientCertificateGenerateSecretPath(accountClient.toRepresentation().getId(), "jwt.credential"), expectedEventRep, ResourceType.CLIENT);

@jimmychakkalakal
jimmychakkalakal marked this pull request as draft August 13, 2026 11:22
@jimmychakkalakal
jimmychakkalakal force-pushed the weakness/49239-Stop-storing-client-private-keys-in-database branch from 180ce8c to 5b38563 Compare August 13, 2026 11:28
@jimmychakkalakal
jimmychakkalakal requested a balanced review from Copilot August 13, 2026 11:44
@jimmychakkalakal
jimmychakkalakal force-pushed the weakness/49239-Stop-storing-client-private-keys-in-database branch from e45883a to c1ca290 Compare August 14, 2026 10:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.

Suppressed comments (9)

services/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java:204

  • This only sanitizes clients created through ClientManager. Both partial import (services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java:122) and realm import (model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultExportImportManager.java:602) call RepresentationToModel.createClient directly, so imported SAML clients still persist both private-key attributes; apply the stripping in the shared creation/import path as well.
        stripPrivateKeys(newClient, clientRep);

services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java:188

  • Malformed JWKS never reaches this new handler: jwksStringToSigCertificateRepresentation wraps parse failures in IllegalStateException, so the preceding catch returns certificate-not-found instead of invalid-jwks. Either recognize an IOException cause in the preceding catch or make the helper consistently throw IllegalArgumentException for parse failures.
        } catch (IllegalArgumentException iae) {
            throw new ErrorResponseException("invalid-jwks", "Could not parse the provided JWKS", Response.Status.BAD_REQUEST);

services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java:141

  • The recommended replacement is not permission-compatible with this endpoint: uploadJks() requires configure, while uploadJksCertificate() requires manage. Under FGAP v1, a caller granted only the configure scope will receive 403 after following this deprecation guidance; align the replacement permission or retain a migration path.
     * @deprecated Use {@link #uploadJksCertificate()} instead.

integration/admin-client-core/src/main/java/org/keycloak/admin/client/resource/ClientAttributeCertificateResource.java:86

  • The preceding API contract still says this method downloads a keystore containing a private key, but the server now deliberately returns only a certificate. Update the Java admin-client documentation so consumers do not continue expecting a key entry.
     * @deprecated Clients should manage their own private keys.

docs/documentation/server_admin/topics/clients/oidc/con-confidential-client-credentials.adoc:62

  • This promises an example conversion to JWK, but the example stops after extracting PEM and leaves the actual conversion as “using a tool of your choice.” It therefore does not provide the keystore-to-JWK example called out by the PR/issue; include a concrete conversion command/tool that produces the shown n and e values.
. Convert the keystore to JWK format. You can use some library or tool according to your preference. For example, using `openssl` and a JWK conversion tool:
+
[source,bash]
----
# Extract the public key in PEM format

services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java:184

  • The existing changed JWKS upload test uses the public-only clientreg-test/jwks.json, so the new stripping logic is never exercised. Add coverage that uploads/registers a JWKS containing private RSA/EC parameters and verifies they are absent from the stored attribute, getKeyInfo, and the admin event.
                Set<String> privateKeyClaims = getAllJwkPrivateKeyClaims(sessionFactory);
                for (JWK key : keySet.getKeys()) {
                    key.getOtherClaims().keySet().removeAll(privateKeyClaims);
                }
                return JsonSerialization.writeValueAsPrettyString(keySet);

js/apps/admin-ui/src/clients/keys/ImportKeyDialog.tsx:34

  • The default caller in Keys.tsx does not pass a title, so this import dialog now shows the import description under the contradictory title “Generate keys?”. Default the title to the existing importKeys message.
  description = "importKeysDescription",

js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties:821

  • The final deprecation guidance is duplicated, so the dialog displays “Key generation is deprecated. Keys should be imported instead.” twice.
generateKeysDescription=If you generate new keys, you can download the keystore with the private key automatically and save it on your client's side. Keycloak server will save just the certificate and public key, but not the private key. Key generation is deprecated. Keys should be imported instead. Key generation is deprecated. Keys should be imported instead.

services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java:163

  • Malformed JWKS never reaches this new handler: jwksStringToSigCertificateRepresentation wraps parse failures in IllegalStateException, so the preceding catch returns certificate-not-found instead of invalid-jwks. Either recognize an IOException cause in the preceding catch or make the helper consistently throw IllegalArgumentException for parse failures.

This issue also appears on line 187 of the same file.

        } catch (IllegalArgumentException iae) {
            throw new ErrorResponseException("invalid-jwks", "Could not parse the provided JWKS", Response.Status.BAD_REQUEST);

The generate endpoint stored client private keys as clear-text client attributes. The server never uses these keys for any cryptographic operation — it only needs the public key/certificate to verify signature. This change:
  - Stops persisting private keys in generate, upload,
    and SAML client creation endpoints
  - Strips private keys from getKeyInfo responses
  - Deprecates the generate, download, and
    generate-and-download endpoints
  - Updates admin UI to reflect that private keys are
    downloaded once and not stored
  - Updates documentation with guidance to generate keys
    externally and examples for converting keystores to
    JWK format for JWKS_URL usage

Closes keycloak#49239

Signed-off-by: Jimmy Chakkalakal <jimmy.chakkalakal@ibm.com>
The generate endpoint stored client private keys as clear-text client attributes. The server never uses these keys for any cryptographic operation — it only needs the public key/certificate to verify signature. This change:
  - Stops persisting private keys in generate, upload,
    and SAML client creation endpoints
  - Strips private keys from getKeyInfo responses
  - Deprecates the generate, download, and
    generate-and-download endpoints
  - Updates admin UI to reflect that private keys are
    downloaded once and not stored
  - Updates documentation with guidance to generate keys
    externally and examples for converting keystores to
    JWK format for JWKS_URL usage

Closes keycloak#49239

Signed-off-by: Jimmy Chakkalakal <jimmy.chakkalakal@ibm.com>
The generate endpoint stored client private keys as clear-text client attributes. The server never uses these keys for any cryptographic operation — it only needs the public key/certificate to verify signature. This change:
  - Stops persisting private keys in generate, upload,
    and SAML client creation endpoints
  - Strips private keys from getKeyInfo responses
  - Deprecates the generate, download, and
    generate-and-download endpoints
  - Updates admin UI to reflect that private keys are
    downloaded once and not stored
  - Updates documentation with guidance to generate keys
    externally and examples for converting keystores to
    JWK format for JWKS_URL usage

Closes keycloak#49239

Signed-off-by: Jimmy Chakkalakal <jimmy.chakkalakal@ibm.com>
Closes keycloak#49239

Signed-off-by: Jimmy Chakkalakal <jimmy.chakkalakal@ibm.com>
Copilot AI review requested due to automatic review settings August 14, 2026 12:03
@jimmychakkalakal
jimmychakkalakal force-pushed the weakness/49239-Stop-storing-client-private-keys-in-database branch from c1ca290 to 4f08761 Compare August 14, 2026 12:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.

Suppressed comments (7)

services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java:216

  • The method now returns a certificate-only keystore, but this Javadoc still says it contains a private key. Update it so generated API documentation does not promise key material that can no longer be retrieved.
     * @deprecated Clients should manage their own private keys.

integration/admin-client-core/src/main/java/org/keycloak/admin/client/resource/ClientAttributeCertificateResource.java:86

  • The admin-client contract still documents this download as containing a private key, although the endpoint now emits only the public certificate. Correct the summary to avoid misleading Java API consumers.
     * @deprecated Clients should manage their own private keys.

js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties:821

  • The deprecation guidance is duplicated verbatim, so the Admin UI renders the same two sentences twice. Remove the repeated copy.
generateKeysDescription=If you generate new keys, you can download the keystore with the private key automatically and save it on your client's side. Keycloak server will save just the certificate and public key, but not the private key. Key generation is deprecated. Keys should be imported instead. Key generation is deprecated. Keys should be imported instead.

services/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java:215

  • There is no regression test creating a SAML client with supplied signing/encryption private-key attributes and asserting that both the model and create admin event are sanitized. Add this case to protect the new creation-path guarantee.
    private static void stripPrivateKeys(ClientModel model, ClientRepresentation rep) {
        model.removeAttribute(SamlConfigAttributes.SAML_SIGNING_PRIVATE_KEY);
        model.removeAttribute(SamlConfigAttributes.SAML_ENCRYPTION_PRIVATE_KEY_ATTRIBUTE);

services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java:141

  • The deprecation points configure-only admins to an endpoint that requires manage: this method allows requireConfigure, while uploadJksCertificate() uses requireManage, and FGAP v1's configure permission does not imply manage. Such callers will receive 403 after migrating; align the replacement's permission or explicitly retain a configure-capable replacement.
     * @deprecated Use {@link #uploadJksCertificate()} instead.

docs/documentation/server_admin/topics/clients/oidc/con-confidential-client-credentials.adoc:58

  • This promises a keystore-to-JWK conversion example, but the commands stop at publickey.pem and leave the actual conversion as “using a tool of your choice.” Add a concrete command with a named conversion tool so the documented workflow—and the PR's stated JWKS conversion guidance—is executable.
. Convert the keystore to JWK format. You can use some library or tool according to your preference. For example, using `openssl` and a JWK conversion tool:

services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java:176

  • The security-critical sanitizer is only exercised with the existing public-only clientreg-test/jwks.json; no regression test supplies private RSA/EC/OKP/octet claims and verifies that storage and getKeyInfo omit them. Add coverage with private JWK input so future claim-handling changes cannot reintroduce persistence.
    public static String stripPrivateKeyParams(KeycloakSessionFactory sessionFactory, String jwks) {

@mposolda mposolda left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jimmychakkalakal Thanks for the latest updates!

I've replied to your inline comment #51612 (comment) and also added one additional inline comment in SamlKeysDialog.tsx .

Also just a note that it is still needed to update also upgrading guide (See my previous comment for the details #51612 (review) )

@jimmychakkalakal

Copy link
Copy Markdown
Contributor Author

Also it is please possible to update also upgrading guide (file changes-26_8_0.adoc) and include this deprecation inside this file as well?

Thanks @mposolda I have updated the guide (file changes-26_8_0.adoc), mentioned the deprecated endpoints under section "Deprecated features"

Copilot AI review requested due to automatic review settings August 14, 2026 15:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 24 out of 24 changed files in this pull request and generated 1 comment.

Suppressed comments (7)

services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java:188

  • Malformed JWKS never reaches this new handler: jwksStringToSigCertificateRepresentation wraps parse IOExceptions in IllegalStateException, so the preceding catch reports certificate-not-found instead of invalid-jwks. Distinguish parse failures in that catch so clients receive the intended error.
        } catch (IllegalArgumentException iae) {
            throw new ErrorResponseException("invalid-jwks", "Could not parse the provided JWKS", Response.Status.BAD_REQUEST);

js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties:821

  • The deprecation guidance is duplicated verbatim in this user-facing description.
generateKeysDescription=If you generate new keys, you can download the keystore with the private key automatically and save it on your client's side. Keycloak server will save just the certificate and public key, but not the private key. Key generation is deprecated. Keys should be imported instead. Key generation is deprecated. Keys should be imported instead.

docs/documentation/upgrading/topics/changes/changes-26_8_0.adoc:75

  • This now describes behavior the endpoint no longer has: getCertificateFromClient omits private keys, so /download returns a certificate-only keystore. Documenting a private-key download could cause administrators to rely on a key that is absent.
* `POST .../download` -- Downloads a keystore containing the client's private key. Clients should manage their own private keys.

integration/admin-client-core/src/main/java/org/keycloak/admin/client/resource/ClientAttributeCertificateResource.java:86

  • The public admin-client API still promises that this endpoint returns a private key, but the server now builds a certificate-only keystore. Update the method summary so generated client documentation reflects the new contract.
     * Get a keystore file for the client, containing private key and public certificate
     * @deprecated Clients should manage their own private keys.

docs/documentation/server_admin/topics/clients/oidc/con-confidential-client-credentials.adoc:60

  • The promised conversion example never identifies or invokes a JWK conversion tool; it stops at a PEM file and a JSON skeleton with placeholder values. This does not satisfy the PR/issue requirement to provide an actionable keystore-to-JWK example, so include a concrete supported command/tool that produces the JWKS.
. Convert the keystore to JWK format. You can use some library or tool according to your preference. For example, using `openssl` and a JWK conversion tool:
+
[source,bash]

services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java:164

  • Malformed JWKS never reaches this new handler: jwksStringToSigCertificateRepresentation wraps parse IOExceptions in IllegalStateException, so the preceding catch reports certificate-not-found instead of invalid-jwks. Distinguish parse failures in that catch so clients receive the intended error.

This issue also appears on line 187 of the same file.

        } catch (IllegalArgumentException iae) {
            throw new ErrorResponseException("invalid-jwks", "Could not parse the provided JWKS", Response.Status.BAD_REQUEST);

services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java:184

  • The existing JWKS upload test uses clientreg-test/jwks.json, which contains only public keys, so none of this security-critical stripping is exercised. Add a regression test with the RSA, EC/OKP, and oct private parameters and assert that persisted JWKS, getKeyInfo, and admin-event data contain none of them.
                Set<String> privateKeyClaims = getAllJwkPrivateKeyClaims(sessionFactory);
                for (JWK key : keySet.getKeys()) {
                    key.getOtherClaims().keySet().removeAll(privateKeyClaims);
                }
                return JsonSerialization.writeValueAsPrettyString(keySet);

Closes keycloak#49239

Signed-off-by: Jimmy Chakkalakal <jimmy.chakkalakal@ibm.com>
Copilot AI review requested due to automatic review settings August 14, 2026 16:07
@jimmychakkalakal
jimmychakkalakal force-pushed the weakness/49239-Stop-storing-client-private-keys-in-database branch from ea27dca to e69b74e Compare August 14, 2026 16:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 24 out of 24 changed files in this pull request and generated no new comments.

Suppressed comments (8)

services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java:188

  • Malformed JWKS JSON is converted to IllegalStateException by jwksStringToSigCertificateRepresentation, so it is handled by the preceding certificate-not-found branch rather than this new invalid-jwks branch. Use a distinct parse exception (or inspect the cause) so malformed JWKS receives the intended error.
        } catch (IllegalArgumentException iae) {
            throw new ErrorResponseException("invalid-jwks", "Could not parse the provided JWKS", Response.Status.BAD_REQUEST);

services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java:216

  • The Javadoc still claims this endpoint returns a private key, while the implementation now builds a certificate-only keystore. Keep the public API documentation consistent with the changed response.
     * @deprecated Clients should manage their own private keys.

services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java:163

  • Malformed JWKS JSON is converted to IllegalStateException by jwksStringToSigCertificateRepresentation, so it is handled by the preceding certificate-not-found branch rather than this new invalid-jwks branch. Use a distinct parse exception (or inspect the cause) so malformed JWKS receives the intended error.

This issue also appears on line 187 of the same file.

        } catch (IllegalArgumentException iae) {
            throw new ErrorResponseException("invalid-jwks", "Could not parse the provided JWKS", Response.Status.BAD_REQUEST);

services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java:183

  • The existing JWKS upload fixture (clientreg-test/jwks.json) contains only public parameters, so the new security-critical stripping path is not exercised. Add a regression test with RSA/EC private claims and verify the stored attribute, getKeyInfo response, and admin event contain none of them.
                Set<String> privateKeyClaims = getAllJwkPrivateKeyClaims(sessionFactory);
                for (JWK key : keySet.getKeys()) {
                    key.getOtherClaims().keySet().removeAll(privateKeyClaims);
                }

services/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java:204

  • There is no regression test that creates a SAML client with the two private-key attributes and verifies both the persisted model and CREATE admin event are sanitized. Add that coverage so this security boundary cannot regress.
        stripPrivateKeys(newClient, clientRep);

js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties:821

  • The final deprecation guidance is duplicated verbatim in this user-facing message. Remove the repeated sentence pair.
generateKeysDescription=If you generate new keys, you can download the keystore with the private key automatically and save it on your client's side. Keycloak server will save just the certificate and public key, but not the private key. Key generation is deprecated. Keys should be imported instead. Key generation is deprecated. Keys should be imported instead.

docs/documentation/upgrading/topics/changes/changes-26_8_0.adoc:75

  • This description still says /download returns the client's private key, but getKeyInfo now always omits it and the endpoint creates a certificate-only keystore. Document the new behavior to avoid telling upgraders they can retrieve a key that is no longer available.
* `POST .../download` -- Downloads a keystore containing the client's private key. Clients should manage their own private keys.

integration/admin-client-core/src/main/java/org/keycloak/admin/client/resource/ClientAttributeCertificateResource.java:86

  • The deprecation notice is attached to stale API documentation claiming this method downloads a private key, although the server now returns a certificate-only keystore. Update the summary so admin-client users are not given an impossible contract.
     * @deprecated Clients should manage their own private keys.

@keycloak-github-bot keycloak-github-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unreported flaky test detected, please review

@keycloak-github-bot

Copy link
Copy Markdown

Unreported flaky test detected

If the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR.

org.keycloak.testsuite.webauthn.registration.WebAuthnOtherSettingsTest#excludeCredentials

Keycloak CI - WebAuthn IT

org.openqa.selenium.NoSuchElementException: 
no such element: Unable to locate element: {"method":"link text","selector":"Register"}
  (Session info: chrome=151.0.7922.108)
For documentation on this error, please visit: https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#no-such-element-exception
Build info: version: '4.28.1', revision: '73f5ad48a2'
...

Report flaky test

@mposolda mposolda left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@mposolda
mposolda merged commit 1ae1dba into keycloak:main Aug 17, 2026
97 of 100 checks passed
lhanusov added a commit to lhanusov/keycloak that referenced this pull request Nov 13, 2026


Signed-off-by: Lukas Hanusovsky <lhanusov@redhat.com>
lhanusov pushed a commit to lhanusov/keycloak that referenced this pull request Nov 13, 2026
Backport the client private-key storage changes to release/26.6 after the certificate-only download fix.
Use the standard private JWK claim names because 26.6 does not provide the newer signature-provider claim discovery API; retain existing helper signatures and verify filtering for RSA, EC, OKP, and symmetric JWKs.
Adapt the test fixture references and publish the upgrade guidance under 26.6.9.

Closes keycloak#49239

(cherry picked from commit 1ae1dba)

Signed-off-by: Jimmy Chakkalakal <jimmy.chakkalakal@ibm.com>
Signed-off-by: Lukas Hanusovsky <lhanusov@redhat.com>
lhanusov added a commit to lhanusov/keycloak that referenced this pull request Nov 13, 2026


Signed-off-by: Lukas Hanusovsky <lhanusov@redhat.com>
lhanusov pushed a commit to lhanusov/keycloak that referenced this pull request Nov 13, 2026
Backport the client private-key storage changes to release/26.6 after the certificate-only download fix.
Use the standard private JWK claim names because 26.6 does not provide the newer signature-provider claim discovery API; retain existing helper signatures and verify filtering for RSA, EC, OKP, and symmetric JWKs.
Adapt the test fixture references and publish the upgrade guidance under 26.6.9.

Closes keycloak#49239

(cherry picked from commit 1ae1dba)

Signed-off-by: Jimmy Chakkalakal <jimmy.chakkalakal@ibm.com>
Signed-off-by: Lukas Hanusovsky <lhanusov@redhat.com>
lhanusov pushed a commit to lhanusov/keycloak that referenced this pull request Nov 13, 2026
Backport the client private-key storage changes to release/26.6 after the certificate-only download fix.
Use the standard private JWK claim names because 26.6 does not provide the newer signature-provider claim discovery API; retain existing helper signatures and verify filtering for RSA, EC, OKP, and symmetric JWKs.
Adapt the test fixture references and publish the upgrade guidance under 26.6.9.

Closes keycloak#49239

(cherry picked from commit 1ae1dba)

Signed-off-by: Jimmy Chakkalakal <jimmy.chakkalakal@ibm.com>
Signed-off-by: Lukas Hanusovsky <lhanusov@redhat.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stop storing client private keys in the database and deprecate generate endpoints

3 participants