diff --git a/apps/admin-ui/public/resources/en/clients-help.json b/apps/admin-ui/public/resources/en/clients-help.json index a1636de09b..a5852ebd78 100644 --- a/apps/admin-ui/public/resources/en/clients-help.json +++ b/apps/admin-ui/public/resources/en/clients-help.json @@ -128,6 +128,7 @@ "archiveFormat": "Java keystore or PKCS12 archive format.", "keyAlias": "Archive alias for your private key and certificate.", "keyPassword": "Password to access the private key in the archive", + "realmCertificateAlias": "Realm certificate is stored in archive too. This is the alias to it.", "storePassword": "Password to access the archive itself", "consentRequired": "If enabled, users have to consent to client access.", "displayOnClient": "Applicable only if 'Consent Required' is on for this client. If this switch is off, the consent screen will contain just the consents corresponding to configured client scopes. If on, there will be also one item on the consent screen about this client itself.", diff --git a/apps/admin-ui/public/resources/en/clients.json b/apps/admin-ui/public/resources/en/clients.json index 318bdf3cd8..586e79c343 100644 --- a/apps/admin-ui/public/resources/en/clients.json +++ b/apps/admin-ui/public/resources/en/clients.json @@ -396,6 +396,10 @@ "generate": "Generate", "import": "Import" }, + "realmCertificateAlias": "Realm certificate alias", + "exportSamlKeyTitle": "Export SAML Keys", + "samlKeysExportSuccess": "Successfully exported keys", + "samlKeysExportError": "Could not export keys due to: {{error}}", "confirm": "Confirm", "browse": "Browse", "importKey": "Import key", diff --git a/apps/admin-ui/src/clients/keys/ExportSamlKeyDialog.tsx b/apps/admin-ui/src/clients/keys/ExportSamlKeyDialog.tsx new file mode 100644 index 0000000000..ad7ee92db0 --- /dev/null +++ b/apps/admin-ui/src/clients/keys/ExportSamlKeyDialog.tsx @@ -0,0 +1,91 @@ +import { useTranslation } from "react-i18next"; +import { FormProvider, useForm } from "react-hook-form"; +import { Button, Modal, Form } from "@patternfly/react-core"; +import FileSaver from "file-saver"; + +import KeyStoreConfig from "@keycloak/keycloak-admin-client/lib/defs/keystoreConfig"; +import { KeyForm } from "./GenerateKeyDialog"; +import { useRealm } from "../../context/realm-context/RealmContext"; +import { useAdminClient } from "../../context/auth/AdminClient"; +import { useAlerts } from "../../components/alert/Alerts"; + +type ExportSamlKeyDialogProps = { + clientId: string; + close: () => void; +}; + +export const ExportSamlKeyDialog = ({ + clientId, + close, +}: ExportSamlKeyDialogProps) => { + const { t } = useTranslation("clients"); + const { realm } = useRealm(); + + const { adminClient } = useAdminClient(); + const { addAlert, addError } = useAlerts(); + + const form = useForm({ + defaultValues: { realmAlias: realm }, + }); + + const download = async (config: KeyStoreConfig) => { + try { + const keyStore = await adminClient.clients.downloadKey( + { + id: clientId, + attr: "saml.signing", + }, + config + ); + FileSaver.saveAs( + new Blob([keyStore], { type: "application/octet-stream" }), + `keystore.${config.format == "PKCS12" ? "p12" : "jks"}` + ); + addAlert(t("samlKeysExportSuccess")); + close(); + } catch (error) { + addError("clients:samlKeysExportError", error); + } + }; + + return ( + + {t("common:export")} + , + , + ]} + > +
+ + + +
+
+ ); +}; diff --git a/apps/admin-ui/src/clients/keys/GenerateKeyDialog.tsx b/apps/admin-ui/src/clients/keys/GenerateKeyDialog.tsx index 56b54f9f6a..72e65cf947 100644 --- a/apps/admin-ui/src/clients/keys/GenerateKeyDialog.tsx +++ b/apps/admin-ui/src/clients/keys/GenerateKeyDialog.tsx @@ -33,9 +33,10 @@ type GenerateKeyDialogProps = { type KeyFormProps = { useFile?: boolean; + isSaml?: boolean; }; -export const KeyForm = ({ useFile = false }: KeyFormProps) => { +export const KeyForm = ({ isSaml = false, useFile = false }: KeyFormProps) => { const { t } = useTranslation("clients"); const [filename, setFilename] = useState(); @@ -113,7 +114,7 @@ export const KeyForm = ({ useFile = false }: KeyFormProps) => { /> )} - + ); }; diff --git a/apps/admin-ui/src/clients/keys/SamlKeys.tsx b/apps/admin-ui/src/clients/keys/SamlKeys.tsx index e3505cac53..0ab8d84d30 100644 --- a/apps/admin-ui/src/clients/keys/SamlKeys.tsx +++ b/apps/admin-ui/src/clients/keys/SamlKeys.tsx @@ -28,6 +28,8 @@ import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog" import { useAlerts } from "../../components/alert/Alerts"; import { SamlImportKeyDialog } from "./SamlImportKeyDialog"; import { convertAttributeNameToForm } from "../../util"; +import useToggle from "../../utils/useToggle"; +import { ExportSamlKeyDialog } from "./ExportSamlKeyDialog"; type SamlKeysProps = { clientId: string; @@ -51,6 +53,7 @@ const KEYS_MAPPING: { [key in KeyTypes]: { [index: string]: string } } = { }; type KeySectionProps = { + clientId: string; keyInfo?: CertificateRepresentation; attr: KeyTypes; onChanged: (key: KeyTypes) => void; @@ -59,6 +62,7 @@ type KeySectionProps = { }; const KeySection = ({ + clientId, keyInfo, attr, onChanged, @@ -71,9 +75,14 @@ const KeySection = ({ const key = KEYS_MAPPING[attr].key; const name = KEYS_MAPPING[attr].name; + const [showImportDialog, toggleImportDialog] = useToggle(); + const section = watch(name); return ( <> + {showImportDialog && ( + + )} {t(`${title}Explain`)} @@ -132,7 +141,9 @@ const KeySection = ({ - + @@ -244,6 +255,7 @@ export const SamlKeys = ({ clientId, save }: SamlKeysProps) => { /> )} { const { t } = useTranslation("clients"); - const { register } = useFormContext(); + const { register, errors } = useFormContext(); return ( <> @@ -27,6 +29,8 @@ export const StoreSettings = ({ fieldLabelId="clients:keyAlias" /> } + helperTextInvalid={t("common:required")} + validated={errors.keyAlias ? "error" : "default"} > {!hidePassword && ( @@ -47,12 +52,35 @@ export const StoreSettings = ({ fieldLabelId="clients:keyPassword" /> } + helperTextInvalid={t("common:required")} + validated={errors.keyPassword ? "error" : "default"} > + + )} + {isSaml && ( + + } + > + )} @@ -66,12 +94,15 @@ export const StoreSettings = ({ fieldLabelId="clients:storePassword" /> } + helperTextInvalid={t("common:required")} + validated={errors.storePassword ? "error" : "default"} >