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
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,11 @@
*/
public class CertificateRepresentation {

protected String privateKey;
protected String publicKey;
protected String certificate;
protected String kid;
protected String jwks;

public String getPrivateKey() {
return privateKey;
}

public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}

public String getPublicKey() {
return publicKey;
}
Expand Down
24 changes: 24 additions & 0 deletions docs/documentation/upgrading/topics/changes/changes-27_0_0.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// ------------------------ Breaking changes ------------------------ //
== Breaking changes

Breaking changes are identified as those that might require changes for existing users to their configurations or applications.
In minor or patch releases, {project_name} will only introduce breaking changes to fix bugs.

=== Client private key endpoints removed

The following Admin REST API endpoints have been removed.
They were deprecated in Keycloak 26.x:

* `POST /admin/realms/{realm}/clients/{id}/certificates/{attr}/generate`
* `POST /admin/realms/{realm}/clients/{id}/certificates/{attr}/download`
* `POST /admin/realms/{realm}/clients/{id}/certificates/{attr}/generate-and-download`

Clients should generate their own key pairs and upload only the public key or
certificate using the existing upload endpoints.

The `privateKey` field has been removed from the `CertificateRepresentation`
API response.

A database migration automatically removes any previously stored client private
key attributes (`saml.signing.private.key`, `saml.encryption.private.key`,
`jwt.credential.private.key`) from the `CLIENT_ATTRIBUTES` table.
4 changes: 4 additions & 0 deletions docs/documentation/upgrading/topics/changes/changes.adoc
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
[[migration-changes]]
== Migration Changes

=== Migrating to 27.0.0

include::changes-27_0_0.adoc[leveloffset=2]

=== Migrating to 26.8.0

include::changes-26_8_0.adoc[leveloffset=2]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import org.keycloak.representations.KeyStoreConfig;
import org.keycloak.representations.idm.CertificateRepresentation;

/**
Expand All @@ -42,15 +41,6 @@ public interface ClientAttributeCertificateResource {
@Produces(MediaType.APPLICATION_JSON)
CertificateRepresentation getKeyInfo();

/**
* Generate a new certificate with new key pair
*
* @return
*/
@POST
@Path("generate")
@Produces(MediaType.APPLICATION_JSON)
CertificateRepresentation generate();

/**
* Upload certificate and eventually private key
Expand All @@ -75,32 +65,5 @@ public interface ClientAttributeCertificateResource {
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
CertificateRepresentation uploadJksCertificate(Object output);

/**
* Get a keystore file for the client, containing private key and public certificate
*
* @param config Keystore configuration as JSON. Parameters "keySize" and "validity" of the config are supported since Keycloak 26.3. Key size is 4096 by default and validity is 3 years by default.
* For older versions than Keycloak 26.3, the key size is 2048 and validity is 10 years.
* @return
*/
@POST
@Path("/download")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
@Consumes(MediaType.APPLICATION_JSON)
byte[] getKeystore(final KeyStoreConfig config);

/**
* Generate a new keypair and certificate, and get the private key file
*
* Generates a keypair and certificate and serves the private key in a specified keystore format.
* Only generated public certificate is saved in Keycloak DB - the private key is not.
*
* @param config Keystore configuration as JSON
* @return
*/
@POST
@Path("/generate-and-download")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
@Consumes(MediaType.APPLICATION_JSON)
byte[] generateAndGetKeystore(final KeyStoreConfig config);

}
94 changes: 0 additions & 94 deletions js/apps/admin-ui/src/clients/keys/ExportSamlKeyDialog.tsx

This file was deleted.

78 changes: 2 additions & 76 deletions js/apps/admin-ui/src/clients/keys/GenerateKeyDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,12 @@ import {
SelectControl,
FileUploadControl,
} from "@keycloak/keycloak-ui-shared";
import {
Button,
ButtonVariant,
Form,
Modal,
ModalVariant,
Text,
TextContent,
} from "@patternfly/react-core";
import { FormProvider, useForm, useFormContext } from "react-hook-form";
import { Form } from "@patternfly/react-core";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { StoreSettings } from "./StoreSettings";

type GenerateKeyDialogProps = {
clientId: string;
toggleDialog: () => void;
save: (keyStoreConfig: KeyStoreConfig) => void;
};

type KeyFormProps = {
useFile?: boolean;
isSaml?: boolean;
Expand Down Expand Up @@ -119,63 +105,3 @@ export const KeyForm = ({
</Form>
);
};

export const GenerateKeyDialog = ({
clientId,
save,
toggleDialog,
}: GenerateKeyDialogProps) => {
const { t } = useTranslation();
const form = useForm<KeyStoreConfig>({
defaultValues: { keyAlias: clientId },
mode: "onChange",
});

const {
handleSubmit,
formState: { isValid },
} = form;

return (
<Modal
variant={ModalVariant.medium}
title={t("generateKeys")}
isOpen
onClose={toggleDialog}
actions={[
<Button
id="modal-confirm"
key="confirm"
data-testid="confirm"
isDisabled={!isValid}
onClick={async () => {
await handleSubmit((config) => {
save(config);
toggleDialog();
})();
}}
>
{t("generate")}
</Button>,
<Button
id="modal-cancel"
key="cancel"
data-testid="cancel"
variant={ButtonVariant.link}
onClick={() => {
toggleDialog();
}}
>
{t("cancel")}
</Button>,
]}
>
<TextContent>
<Text>{t("generateKeysDescription")}</Text>
</TextContent>
<FormProvider {...form}>
<KeyForm />
</FormProvider>
</Modal>
);
};
40 changes: 0 additions & 40 deletions js/apps/admin-ui/src/clients/keys/Keys.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type CertificateRepresentation from "@keycloak/keycloak-admin-client/lib/defs/certificateRepresentation";
import type KeyStoreConfig from "@keycloak/keycloak-admin-client/lib/defs/keystoreConfig";
import { TextControl, useAlerts, useFetch } from "@keycloak/keycloak-ui-shared";
import {
ActionGroup,
Expand All @@ -14,7 +13,6 @@ import {
TextContent,
} from "@patternfly/react-core";
import { SyncAltIcon } from "@patternfly/react-icons";
import { saveAs } from "file-saver";
import { useState } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
Expand All @@ -25,7 +23,6 @@ import { convertAttributeNameToForm } from "../../util";
import useToggle from "../../utils/useToggle";
import { FormFields } from "../ClientDetails";
import { KeyInfoArea } from "./Certificate";
import { GenerateKeyDialog, getFileExtension } from "./GenerateKeyDialog";
import { ImportFile, ImportKeyDialog } from "./ImportKeyDialog";

type KeysProps = {
Expand All @@ -48,14 +45,11 @@ export const Keys = ({
const { t } = useTranslation();
const {
control,
getValues,
formState: { isDirty },
} = useFormContext<FormFields>();
const { addAlert, addError } = useAlerts();

const [keyInfo, setKeyInfo] = useState<CertificateRepresentation>();
const [openGenerateKeys, toggleOpenGenerateKeys, setOpenGenerateKeys] =
useToggle();
const [openImportKeys, toggleOpenImportKeys, setOpenImportKeys] = useToggle();
const [key, setKey] = useState(0);
const refresh = () => {
Expand All @@ -82,26 +76,6 @@ export const Keys = ({
[key],
);

const generate = async (config: KeyStoreConfig) => {
try {
const keyStore = await adminClient.clients.generateAndDownloadKey(
{
id: clientId,
attr,
},
config,
);
saveAs(
new Blob([keyStore], { type: "application/octet-stream" }),
`keystore.${getFileExtension(config.format ?? "")}`,
);
addAlert(t("generateSuccess"), AlertVariant.success);
refresh();
} catch (error) {
addError("generateError", error);
}
};

const importKey = async (importFile: ImportFile) => {
try {
const formData = new FormData();
Expand All @@ -125,13 +99,6 @@ export const Keys = ({

return (
<PageSection variant="light" className="keycloak__form">
{openGenerateKeys && (
<GenerateKeyDialog
clientId={getValues("clientId")!}
toggleDialog={toggleOpenGenerateKeys}
save={generate}
/>
)}
{openImportKeys && (
<ImportKeyDialog toggleDialog={toggleOpenImportKeys} save={importKey} />
)}
Expand Down Expand Up @@ -178,13 +145,6 @@ export const Keys = ({
>
{t("save")}
</Button>
<Button
data-testid="generate"
variant="secondary"
onClick={() => setOpenGenerateKeys(true)}
>
{t("generateNewKeys")}
</Button>
<Button
data-testid="import"
variant="secondary"
Expand Down
Loading
Loading