Settings (SAML) (#1302)

This commit is contained in:
Erik Jan de Wit 2021-10-05 12:32:20 +02:00 committed by GitHub
parent 9180b18652
commit c71d21c748
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 428 additions and 11 deletions

View file

@ -287,4 +287,49 @@ describe("Clients test", () => {
cy.findByTestId("bearer-only-explainer-tooltip").should("exist");
});
});
describe("SAML test", () => {
const samlClientName = "saml";
before(() => {
new AdminClient().createClient({
protocol: "saml",
clientId: samlClientName,
publicClient: false,
});
});
after(() => {
new AdminClient().deleteClient(samlClientName);
});
beforeEach(() => {
keycloakBefore();
loginPage.logIn();
sidebarPage.goToClients();
listingPage.searchItem(samlClientName).goToItemDetails(samlClientName);
});
it("should display the saml sections on details screen", () => {
cy.get(".pf-c-jump-links__list").should(($ul) => {
expect($ul)
.to.contain("SAML capabilities")
.to.contain("Signature and Encryption");
});
});
it("should save force name id format", () => {
const load = "auth/admin/realms/master/client-scopes";
cy.intercept(load).as("load");
cy.get(".pf-c-jump-links__list").contains("SAML capabilities").click();
cy.wait("@load");
cy.findByTestId("forceNameIdFormat").click({
force: true,
});
cy.findByTestId("settingsSave").click();
masthead.checkNotificationMessage("Client successfully updated");
});
});
});

View file

@ -180,7 +180,7 @@ export const ClientDetails = () => {
setChangeAuthenticatorOpen(!changeAuthenticatorOpen);
const [activeTab2, setActiveTab2] = useState(30);
const form = useForm<ClientForm>();
const form = useForm<ClientForm>({ shouldUnregister: false });
const { clientId } = useParams<ClientParams>();
const clientAuthenticatorType = useWatch({
@ -341,7 +341,8 @@ export const ClientDetails = () => {
reset={() => setupForm(client)}
/>
</Tab>
{!client.publicClient && !isRealmClient(client) && (
{((!client.publicClient && !isRealmClient(client)) ||
client.protocol === "saml") && (
<Tab
id="keys"
eventKey="keys"

View file

@ -20,37 +20,53 @@ import { FormAccess } from "../components/form-access/FormAccess";
import { HelpItem } from "../components/help-enabler/HelpItem";
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { SaveReset } from "./advanced/SaveReset";
import { SamlConfig } from "./add/SamlConfig";
import { SamlSignature } from "./add/SamlSignature";
import type { ClientForm } from "./ClientDetails";
type ClientSettingsProps = {
save: () => void;
reset: () => void;
};
const baseSections = [
"generalSettings",
"capabilityConfig",
"accessSettings",
"loginSettings",
] as const;
const samlSections = [
"generalSettings",
"samlCapabilityConfig",
"signatureAndEncryption",
"accessSettings",
"loginSettings",
] as const;
export const ClientSettings = ({ save, reset }: ClientSettingsProps) => {
const { register, control, watch } = useFormContext();
const { register, control, watch } = useFormContext<ClientForm>();
const { t } = useTranslation("clients");
const [loginThemeOpen, setLoginThemeOpen] = useState(false);
const loginThemes = useServerInfo().themes!["login"];
const consentRequired: boolean = watch("consentRequired");
const consentRequired = watch("consentRequired");
const displayOnConsentScreen: string = watch(
"attributes.display-on-consent-screen"
);
const protocol = watch("protocol");
const sections = protocol === "saml" ? samlSections : baseSections;
return (
<ScrollForm
className="pf-u-px-lg"
sections={[
t("generalSettings"),
t("capabilityConfig"),
t("accessSettings"),
t("loginSettings"),
]}
sections={sections.map((section) => t(section))}
>
<Form isHorizontal>
<ClientDescription />
</Form>
<CapabilityConfig />
{protocol === "saml" ? <SamlConfig /> : <CapabilityConfig />}
{protocol === "saml" && <SamlSignature />}
<FormAccess isHorizontal role="manage-clients">
<FormGroup
label={t("rootUrl")}

View file

@ -0,0 +1,124 @@
import React, { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import {
FormGroup,
Select,
SelectOption,
SelectVariant,
Switch,
} from "@patternfly/react-core";
import type { ClientForm } from "../ClientDetails";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
export const Toggle = ({ name, label }: { name: string; label: string }) => {
const { t } = useTranslation("clients");
const { control } = useFormContext<ClientForm>();
return (
<FormGroup
hasNoPaddingTop
label={t(label)}
fieldId={label}
labelIcon={
<HelpItem
helpText={t(`clients-help:${label}`)}
forLabel={t(label)}
forID={label}
/>
}
>
<Controller
name={name}
defaultValue="false"
control={control}
render={({ onChange, value }) => (
<Switch
id={name!}
data-testid={label}
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange(value.toString())}
/>
)}
/>
</FormGroup>
);
};
export const SamlConfig = () => {
const { t } = useTranslation("clients");
const { control } = useFormContext<ClientForm>();
const [nameFormatOpen, setNameFormatOpen] = useState(false);
return (
<FormAccess
isHorizontal
role="manage-clients"
className="keycloak__capability-config__form"
>
<FormGroup
label={t("nameIdFormat")}
fieldId="nameIdFormat"
labelIcon={
<HelpItem
helpText="clients-help:nameIdFormat"
forLabel={t("nameIdFormat")}
forID={t("common:helpLabel", { label: t("nameIdFormat") })}
/>
}
>
<Controller
name="attributes.saml_name_id_format"
defaultValue="username"
control={control}
render={({ onChange, value }) => (
<Select
toggleId="samlNameIdFormat"
onToggle={(open) => setNameFormatOpen(open)}
onSelect={(_, value) => {
onChange(value.toString());
setNameFormatOpen(false);
}}
selections={value}
variant={SelectVariant.single}
aria-label={t("nameIdFormat")}
isOpen={nameFormatOpen}
>
{["username", "email", "transient", "persistent"].map((name) => (
<SelectOption
selected={name === value}
key={name}
value={name}
/>
))}
</Select>
)}
/>
</FormGroup>
<Toggle
name="attributes.saml_force_name_id_format"
label="forceNameIdFormat"
/>
<Toggle
name="attributes.saml-force-post-binding"
label="forcePostBinding"
/>
<Toggle
name="attributes.saml-artifact-binding"
label="forceArtifactBinding"
/>
<Toggle
name="attributes.saml-onetimeuse-condition"
label="includeOneTimeUseCondition"
/>
<Toggle
name="attributes.saml-server-signature-keyinfo-ext"
label="optimizeLookup"
/>
</FormAccess>
);
};

View file

@ -0,0 +1,199 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import {
FormGroup,
Select,
SelectOption,
SelectVariant,
} from "@patternfly/react-core";
import type { ClientForm } from "../ClientDetails";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { Toggle } from "./SamlConfig";
const SIGNATURE_ALGORITHMS = [
"RSA_SHA1",
"RSA_SHA256",
"RSA_SHA256_MGF1",
"RSA_SHA512",
"RSA_SHA512_MGF1",
"DSA_SHA1",
] as const;
const KEYNAME_TRANSFORMER = ["NONE", "KEY_ID", "CERT_SUBJECT"] as const;
const CANONICALIZATION = [
{ name: "EXCLUSIVE", value: "http://www.w3.org/2001/10/xml-exc-c14n#" },
{
name: "EXCLUSIVE_WITH_COMMENTS",
value: "http://www.w3.org/2001/10/xml-exc-c14n#WithComments",
},
{
name: "INCLUSIVE",
value: "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
},
{
name: "INCLUSIVE_WITH_COMMENTS",
value: "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments",
},
] as const;
export const SamlSignature = () => {
const { t } = useTranslation("clients");
const [algOpen, setAlgOpen] = useState(false);
const [keyOpen, setKeyOpen] = useState(false);
const [canOpen, setCanOpen] = useState(false);
const { control, watch } = useFormContext<ClientForm>();
const signDocs = watch("attributes.saml-server-signature");
const signAssertion = watch("attributes.saml-assertion-signature");
return (
<FormAccess
isHorizontal
role="manage-clients"
className="keycloak__capability-config__form"
>
<Toggle name="attributes.saml-server-signature" label="signDocuments" />
<Toggle
name="attributes.saml-assertion-signature"
label="signAssertions"
/>
{(signDocs === "true" || signAssertion === "true") && (
<>
<FormGroup
label={t("signatureAlgorithm")}
fieldId="signatureAlgorithm"
labelIcon={
<HelpItem
helpText="clients-help:signatureAlgorithm"
forLabel={t("signatureAlgorithm")}
forID={t("common:helpLabel", {
label: t("signatureAlgorithm"),
})}
/>
}
>
<Controller
name="attributes.saml-signature-algorithm"
defaultValue={SIGNATURE_ALGORITHMS[0]}
Key
control={control}
render={({ onChange, value }) => (
<Select
toggleId="signatureAlgorithm"
onToggle={(open) => setAlgOpen(open)}
onSelect={(_, value) => {
onChange(value.toString());
setAlgOpen(false);
}}
selections={value}
variant={SelectVariant.single}
aria-label={t("signatureAlgorithm")}
isOpen={algOpen}
>
{SIGNATURE_ALGORITHMS.map((algorithm) => (
<SelectOption
selected={algorithm === value}
key={algorithm}
value={algorithm}
/>
))}
</Select>
)}
/>
</FormGroup>
<FormGroup
label={t("signatureKeyName")}
fieldId="signatureKeyName"
labelIcon={
<HelpItem
helpText="clients-help:signatureKeyName"
forLabel={t("signatureKeyName")}
forID={t("common:helpLabel", {
label: t("signatureKeyName"),
})}
/>
}
>
<Controller
name="attributes.saml-server-signature-keyinfo-xmlSigKeyInfoKeyNameTransformer"
defaultValue={KEYNAME_TRANSFORMER[0]}
control={control}
render={({ onChange, value }) => (
<Select
toggleId="signatureKeyName"
onToggle={(open) => setKeyOpen(open)}
onSelect={(_, value) => {
onChange(value.toString());
setKeyOpen(false);
}}
selections={value}
variant={SelectVariant.single}
aria-label={t("signatureKeyName")}
isOpen={keyOpen}
>
{KEYNAME_TRANSFORMER.map((key) => (
<SelectOption
selected={key === value}
key={key}
value={key}
/>
))}
</Select>
)}
/>
</FormGroup>
<FormGroup
label={t("canonicalization")}
fieldId="canonicalization"
labelIcon={
<HelpItem
helpText="clients-help:canonicalization"
forLabel={t("canonicalization")}
forID={t("common:helpLabel", {
label: t("canonicalization"),
})}
/>
}
>
<Controller
name="attributes.saml_signature_canonicalization_method"
defaultValue={CANONICALIZATION[0].value}
control={control}
render={({ onChange, value }) => (
<Select
toggleId="canonicalization"
onToggle={(open) => setCanOpen(open)}
onSelect={(_, value) => {
onChange(value.toString());
setCanOpen(false);
}}
selections={
CANONICALIZATION.find((can) => can.value === value)?.name
}
variant={SelectVariant.single}
aria-label={t("canonicalization")}
isOpen={canOpen}
>
{CANONICALIZATION.map((can) => (
<SelectOption
selected={can.value === value}
key={can.name}
value={can.value}
>
{can.name}
</SelectOption>
))}
</Select>
)}
/>
</FormGroup>
</>
)}
</FormAccess>
);
};

View file

@ -19,6 +19,25 @@ export default {
rootURL: "Root URL appended to relative URLs",
validRedirectURIs:
"Valid URI pattern a browser can redirect to after a successful login or logout. Simple wildcards are allowed such as 'http://example.com/*'. Relative path can be specified too such as /my/relative/path/*. Relative paths are relative to the client root URL, or if none is specified the auth server root URL is used. For SAML, you must set valid URI patterns if you are relying on the consumer service URL embedded with the login request.",
nameIdFormat: "The name ID format to use for the subject.",
forceNameIdFormat:
"Ignore requested NameID subject format and use admin console configured one.",
forcePostBinding: "Always use POST binding for responses.",
forceArtifactBinding:
"Should response messages be returned to the client through the SAML ARTIFACT binding system?",
includeAuthnStatement:
"Should a statement specifying the method and timestamp be included in login responses?",
includeOneTimeUseCondition:
"Should a OneTimeUse Condition be included in login responses?",
optimizeLookup:
"When signing SAML documents in REDIRECT binding for SP that is secured by Keycloak adapter, should the ID of the signing key be included in SAML protocol message in <Extensions> element? This optimizes validation of the signature as the validating party uses a single key instead of trying every known key for validation.",
signDocuments: "Should SAML documents be signed by the realm?",
signAssertions:
"Should assertions inside SAML documents be signed? This setting is not needed if document is already being signed.",
signatureAlgorithm: "The signature algorithm to use to sign documents.",
signatureKeyName:
"Signed SAML documents contain identification of signing key in KeyName element. For Keycloak / RH-SSO counterparty, use KEY_ID, for MS AD FS use CERT_SUBJECT, for others check and use NONE if no other option works.",
canonicalization: "Canonicalization Method for XML signatures.",
webOrigins:
"Allowed CORS origins. To permit all origins of Valid Redirect URIs, add '+'. This does not include the '*' wildcard though. To permit all origins, explicitly add '*'.",
homeURL:

View file

@ -136,6 +136,19 @@ export default {
accessSettings: "Access settings",
rootUrl: "Root URL",
validRedirectUri: "Valid redirect URIs",
samlCapabilityConfig: "SAML capabilities",
signatureAndEncryption: "Signature and Encryption",
nameIdFormat: "Name ID format",
forceNameIdFormat: "Force name ID format",
forcePostBinding: "Force POST binding",
forceArtifactBinding: "Force artifact binding",
includeAuthnStatement: "Include AuthnStatement",
includeOneTimeUseCondition: "Include OneTimeUse Condition",
optimizeLookup: "Optimize REDIRECT signing key lookup",
signDocuments: "Sign documents",
signAssertions: "Sign assertions",
signatureKeyName: "SAML signature key name",
canonicalization: "Canonicalization method",
addRedirectUri: "Add valid redirect URIs",
loginTheme: "Login theme",
consentRequired: "Consent required",