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
2 changes: 1 addition & 1 deletion .github/workflows/js-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ jobs:
- name: Start Keycloak server
run: |
tar xfvz keycloak-999.0.0-SNAPSHOT.tar.gz
keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=admin-fine-grained-authz:v2,transient-users,spiffe,oid4vc-vci,kubernetes-service-accounts &> ~/server.log &
keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=admin-fine-grained-authz:v2,transient-users,spiffe,oid4vc-vci,kubernetes-service-accounts,jwt-authorization-grant &> ~/server.log &
env:
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class CertificateRepresentation {
protected String publicKey;
protected String certificate;
protected String kid;
protected String jwks;

public String getPrivateKey() {
return privateKey;
Expand Down Expand Up @@ -60,4 +61,12 @@ public String getKid() {
public void setKid(String kid) {
this.kid = kid;
}

public String getJwks() {
return jwks;
}

public void setJwks(String jwks) {
this.jwks = jwks;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,7 @@ deleteMappingTitle=Delete mapping?
profile=Profile
active=Active
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.
importKeysDescription=Import a public key using different file formats. Please select the type of archive you want to import.
addSubFlowTitle=Add a sub-flow
useTruststoreSpiHelp=Specifies whether LDAP connection will use the Truststore SPI with the truststore configured in command-line options. 'Always' means that it will always use it. 'Never' means that it will not use it. Note that even if Keycloak truststore is not configured, the default java cacerts or certificate specified by 'javax.net.ssl.trustStore' property will be used.
forcePostBindingHelp=Always use POST binding for responses.
Expand Down Expand Up @@ -1974,7 +1975,7 @@ scopeParameter=Scope parameter
unsigned=Unsigned
userGroupsRetrieveStrategy=User groups retrieve strategy
addSubFlow=Add sub-flow
validatingPublicKeyHelp=The public key in PEM format that must be used to verify external IDP signatures.
validatingPublicKeyHelp=The public key in PEM or JWKS format that must be used to verify external IDP signatures. The button below can be used to import an external file with different key and certificate formats. The provider needs to be saved after the import to store the changes.
client-uris-must-match.label=Client URIs Must Match
webAuthnPolicyAcceptableAaguids=Acceptable AAGUIDs
noRoles-roles=No roles in this realm
Expand Down
8 changes: 6 additions & 2 deletions js/apps/admin-ui/src/clients/keys/ImportKeyDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { StoreSettings } from "./StoreSettings";
type ImportKeyDialogProps = {
toggleDialog: () => void;
save: (importFile: ImportFile) => void;
title?: string;
description?: string;
};

export type ImportFile = {
Expand All @@ -28,6 +30,8 @@ export type ImportFile = {
export const ImportKeyDialog = ({
save,
toggleDialog,
title = "generateKeys",
description = "generateKeysDescription",
}: ImportKeyDialogProps) => {
const { t } = useTranslation();
const form = useForm<ImportFile>();
Expand All @@ -50,7 +54,7 @@ export const ImportKeyDialog = ({
return (
<Modal
variant={ModalVariant.medium}
title={t("generateKeys")}
title={t(title)}
isOpen
onClose={toggleDialog}
actions={[
Expand Down Expand Up @@ -79,7 +83,7 @@ export const ImportKeyDialog = ({
]}
>
<TextContent>
<Text>{t("generateKeysDescription")}</Text>
<Text>{t(description)}</Text>
</TextContent>
<Form className="pf-v5-u-pt-lg">
<FormProvider {...form}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@ import { ExpandableSection } from "@patternfly/react-core";
import { useState } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import {
SelectControl,
TextAreaControl,
TextControl,
} from "@keycloak/keycloak-ui-shared";
import { SelectControl, TextControl } from "@keycloak/keycloak-ui-shared";
import { DefaultSwitchControl } from "../../components/SwitchControl";
import { JwksSettings } from "./JwksSettings";

import "./discovery-settings.css";

Expand All @@ -27,10 +24,6 @@ const Fields = ({ readOnly, isOIDC }: DiscoverySettingsProps) => {
control,
name: "config.validateSignature",
});
const useJwks = useWatch({
control,
name: "config.useJwksUrl",
});
const isPkceEnabled = useWatch({
control,
name: "config.pkceEnabled",
Expand Down Expand Up @@ -104,38 +97,7 @@ const Fields = ({ readOnly, isOIDC }: DiscoverySettingsProps) => {
{(validateSignature === "true" ||
jwtAuthorizationGrantEnabled === "true" ||
supportsClientAssertions == "true") && (
<>
<DefaultSwitchControl
name="config.useJwksUrl"
label={t("useJwksUrl")}
labelIcon={t("useJwksUrlHelp")}
isDisabled={readOnly}
stringify
/>
{useJwks === "true" ? (
<TextControl
name="config.jwksUrl"
label={t("jwksUrl")}
labelIcon={t("jwksUrlHelp")}
type="url"
readOnly={readOnly}
/>
) : (
<>
<TextAreaControl
name="config.publicKeySignatureVerifier"
label={t("validatingPublicKey")}
labelIcon={t("validatingPublicKeyHelp")}
/>
<TextControl
name="config.publicKeySignatureVerifierKeyId"
label={t("validatingPublicKeyId")}
labelIcon={t("validatingPublicKeyIdHelp")}
readOnly={readOnly}
/>
</>
)}
</>
<JwksSettings readOnly={readOnly} />
)}
</>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useTranslation } from "react-i18next";

import { TextControl, NumberControl } from "@keycloak/keycloak-ui-shared";
import { JWTAuthorizationGrantAssertionSettings } from "./JWTAuthorizationGrantAssertionSettings";
import { Divider } from "@patternfly/react-core";
import { JwksSettings } from "./JwksSettings";

export default function JWTAuthorizationGrantSettings() {
const { t } = useTranslation();
return (
Expand All @@ -22,14 +23,7 @@ export default function JWTAuthorizationGrantSettings() {
required: t("required"),
}}
/>
<TextControl
name="config.jwksUrl"
label={t("jwtAuthorizationGrantJWKSUrl")}
labelIcon={t("jwtAuthorizationGrantJWKSUrlHelp")}
rules={{
required: t("required"),
}}
/>
<JwksSettings />
<JWTAuthorizationGrantAssertionSettings />
<NumberControl
name="config.jwtAuthorizationGrantAllowedClockSkew"
Expand Down
129 changes: 129 additions & 0 deletions js/apps/admin-ui/src/identity-providers/add/JwksSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation";
import { AlertVariant, FormGroup, Button } from "@patternfly/react-core";
import { useFormContext, useWatch } from "react-hook-form";
import { DefaultSwitchControl } from "../../components/SwitchControl";
import { useTranslation } from "react-i18next";
import {
TextAreaControl,
TextControl,
useAlerts,
} from "@keycloak/keycloak-ui-shared";
import {
ImportFile,
ImportKeyDialog,
} from "../../clients/keys/ImportKeyDialog";
import useToggle from "../../utils/useToggle";
import { useAdminClient } from "../../admin-client";

type JwksSettingsProps = {
readOnly?: boolean;
};

export const JwksSettings = ({ readOnly = false }: JwksSettingsProps) => {
const { t } = useTranslation();
const { control, setValue } =
useFormContext<IdentityProviderRepresentation>();
const { adminClient } = useAdminClient();
const { addAlert, addError } = useAlerts();
const [openImportKeys, toggleOpenImportKeys, setOpenImportKeys] = useToggle();
const useJwks = useWatch({
control,
name: "config.useJwksUrl",
defaultValue: "true",
});
const publicKeySignatureVerifier = useWatch({
control,
name: "config.publicKeySignatureVerifier",
});

const importKey = async (importFile: ImportFile) => {
try {
const formData = new FormData();
const { file, ...rest } = importFile;

for (const [key, value] of Object.entries(rest)) {
formData.append(key, value);
}

formData.append("file", file);
const info = await adminClient.identityProviders.uploadCertificate(
{},
formData,
);
if (info.jwks) {
setValue("config.publicKeySignatureVerifier", info.jwks);
setValue("config.publicKeySignatureVerifierKeyId", "");
addAlert(t("importSuccess"), AlertVariant.success);
} else if (info.publicKey) {
setValue("config.publicKeySignatureVerifier", info.publicKey);
addAlert(t("importSuccess"), AlertVariant.success);
} else {
addError("importError", t("emptyResources"));
}
} catch (error) {
addError("importError", error);
}
};

return (
<>
<DefaultSwitchControl
name="config.useJwksUrl"
label={t("useJwksUrl")}
labelIcon={t("useJwksUrlHelp")}
isDisabled={readOnly}
defaultValue={"true"}
stringify
/>
{useJwks === "true" ? (
<TextControl
name="config.jwksUrl"
label={t("jwksUrl")}
labelIcon={t("jwksUrlHelp")}
type="url"
readOnly={readOnly}
rules={{
required: t("required"),
}}
/>
) : (
<>
{openImportKeys && (
<ImportKeyDialog
toggleDialog={toggleOpenImportKeys}
save={importKey}
title="importKey"
description="importKeysDescription"
/>
)}
{!publicKeySignatureVerifier?.trim().startsWith("{") && (
<TextControl
name="config.publicKeySignatureVerifierKeyId"
label={t("validatingPublicKeyId")}
labelIcon={t("validatingPublicKeyIdHelp")}
readOnly={readOnly}
/>
)}
<TextAreaControl
name="config.publicKeySignatureVerifier"
label={t("validatingPublicKey")}
labelIcon={t("validatingPublicKeyHelp")}
rules={{ required: t("required") }}
readOnly={readOnly}
/>
{!readOnly && (
<FormGroup fieldId="kc-import-certificate-button">
<Button
variant="secondary"
data-testid="import-certificate-button"
onClick={() => setOpenImportKeys(true)}
>
{t("import")}
</Button>
</FormGroup>
)}
</>
)}
</>
);
};
Loading
Loading