Allow configure encryption details for SAML clients

Closes #40933

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc
2025-07-03 16:12:49 +02:00
committed by Marek Posolda
parent 52b1c1850e
commit e0bba39da0
30 changed files with 883 additions and 52 deletions

View File

@@ -78,6 +78,8 @@ This option applies to REDIRECT bindings where the signature is transferred in q
+
This option is used when {project_name} server and adapter provide the IDP and SP. This option is only relevant when *Sign Documents* is set to ON.
*Allow ECP Flow*:: If true, this application is allowed to use SAML ECP profile for authentication.
=== Signature and Encryption
*Sign Documents*:: When set to ON, {project_name} signs the document using the realms private key.
@@ -98,6 +100,16 @@ do not work if the SAML client runs on Java 17 or higher.
+
*Canonicalization Method*:: The canonicalization method for XML signatures.
*Encryption algorithm*:: Encryption algorithm used for the client. Default value is `AES_256_GCM` when not defined.
*Key transport algorithm*:: Key transport algorithm used for the client to encrypt the secret key used for encryption. Default value is `RSA-OAEP-11` when not defined.
*Digest method for RSA-OAEP*:: Digest method to use when RSA-OAEP is selected as the key transport algorithm. Only available if *Key transport algorithm* is set to any RSA-OAEP algorithm. Default value is `SHA-256` when not defined.
*Mask generation function*:: Mask generation function to use when `RSA-OAEP-11` is selected as the key transport algorithm. Only available if *Key transport algorithm* is set to `RSA-OAEP-11` algorithm. Default value is `mgf1sha256` when no defined.
NOTE: The encryption options are only available if the *Encrypt Assertions* option is enabled in the *Keys* tab. For more information about SAML/XML encryption, see the link:https://www.w3.org/TR/xmlenc-core1/[XML Encryption Syntax and Processing] specification.
=== Login settings
*Login theme*:: A theme to use for login, OTP, grant registration, and forgotten password pages.
@@ -123,11 +135,9 @@ There will be also one item on the consent screen about this client itself.
== Keys tab
*Encrypt Assertions*:: Encrypts the assertions in SAML documents with the realms private key. The AES algorithm uses a key size of 128 bits.
*Client Signature Required*:: If *Client Signature Required* is enabled, documents coming from a client are expected to be signed. {project_name} will validate this signature using the client public key or cert set up in the `Keys` tab.
*Allow ECP Flow*:: If true, this application is allowed to use SAML ECP profile for authentication.
*Encrypt Assertions*:: Encrypts the assertions in SAML documents with the specified client public key. Default algorithms used for encryption are configured with security in mind. If you need a different configuration, the encryption details can be modified in the *Settings* tab, section *Signature and Encryption*. The encryption options are only visible when this *Encrypt Assertions* option is enabled.
== Advanced tab
@@ -157,3 +167,6 @@ This tab has many fields for specific situations. Some fields are covered in ot
*Artifact Resolution Service*:: URL of the client SOAP endpoint where to send the `ArtifactResolve` messages to.
=== Advanced settings
*Assertion Lifespan*:: Specific client lifespan set in the SAML assertion conditions. After that time the assertion will be invalid. If not specified the realm *Access Token Lifespan* is used. The `SessionNotOnOrAfter` attribute is not modified and continue using the *SSO Session Max* time defined at realm level.

View File

@@ -55,6 +55,14 @@ The feature is enabled for a realm, if `Update Email` required action is enabled
The feature slightly changes behaviour from previous versions when updating the profile during the authentication flow (e.g. when running the `UPDATE_PROFILE` required action).
If an existing user does have an email set when updating the profile during the authentication flow, the email attribute will not be available.
=== Encryption algorithms for SAML updated
When a SAML client was enabled to *Encrypt Assertions*, the assertion included in the SAML response was encrypted following the link:https://www.w3.org/TR/xmlenc-core1/[XML Encryption Syntax and Processing] specification. The algorithms used for encryption were fixed and outdated. Since this release, default encryption options are up to date and better suited in terms of security. Besides, the encryption details are also configurable, just in case a specific client needs a different set of algorithms to work properly. New attributes can be defined in the client to specify the exact algorithms used for encryption. The Admin console displays them in the client tab *Settings*, section *Signature and Encryption*, when the *Encrypt Assertions* option is enabled in the *Keys* tab.
In order to maintain backwards compatibility, {project_name}'s upgrade will modify the existing SAML clients to set the encryption attributes to work as before. This way old clients will receive the same encrypted assertion using the same previous algorithms. If the client supports the new default configuration, removing the attributes is recommended.
For more information about client configuration, please see link:{adminguide_link}#_client-saml-configuration[Creating a SAML client] chapter in the {adminguide_name}.
// ------------------------ Deprecated features ------------------------ //
== Deprecated features

View File

@@ -1592,7 +1592,7 @@ synchronizationSettings=Synchronization settings
certificateHelp=Client Certificate for validate JWT issued by client and signed by Client private key from your keystore.
resetPasswordError=Error resetting password\: {{error}}
associatedPermissions=Associated permission
encryptionKeysConfigExplain=If you enable the "Encryption assertions" below, you must configure the encryption keys by generating or importing keys, and the SAML assertions will be encrypted with the client's public key using AES.
encryptionKeysConfigExplain=If you enable the "Encryption assertions" below, you must configure the encryption keys by generating or importing keys, and the SAML assertions will be encrypted with the client's public key. When enabled, the encryption details can be modified in the "Settings" tab, section "Signature and Encryption".
preserveGroupInheritanceHelp=Flag whether group inheritance from LDAP should be propagated to Keycloak. If false, then all LDAP groups will be mapped as flat top-level groups in Keycloak. Otherwise group inheritance is preserved into Keycloak, but the group sync might fail if LDAP structure contains recursions or multiple parent groups per child groups.
createScopeBasedPermission=Create scope-based permission
showMore=Show more
@@ -1917,7 +1917,7 @@ eventTypes.IDENTITY_PROVIDER_LOGIN_ERROR.name=Identity provider login error
scopePermissions.groups.view-description=Policies that decide if an administrator can view this group
tokens=Tokens
createFlow=Create flow
encryptAssertionsHelp=Should SAML assertions be encrypted with client's public key using AES?
encryptAssertionsHelp=Should SAML assertions be encrypted with client's public key?
oAuthDPoPHelp=This enables support for Demonstrating Proof-of-Possession (DPoP) bound tokens. The access and refresh tokens are bound to the key stored on the user agent. In order to prove the possession of the key, the user agent must send a signed proof alongside the token.
unsavedChangesConfirm=You have unsaved changes. Do you really want to leave the page?
disabledOff=Disabled off
@@ -3501,9 +3501,17 @@ givenNameClaim=Given name Claim
givenNameClaimHelp=The name of the claim from the JSON document returned by the user profile endpoint representing the user's given name. If not provided, defaults to `given_name`.
familyNameClaim=Family name Claim
familyNameClaimHelp=The name of the claim from the JSON document returned by the user profile endpoint representing the user's family name. If not provided, defaults to `family_name`.
samlClientEncryptionAlgorithm=Encryption algorithm
samlClientEncryptionAlgorithmHelp=Encryption algorithm used for the client. Default AES_256_GCM.
samlClientKeyEncryptionAlgorithm=Key transport algorithm
samlClientKeyEncryptionAlgorithmHelp=Key transport algorithm used for the client to encrypt the secret key used for encryption. Default value RSA-OAEP-11.
samlClientEncryptionDigestMethod=Digest method for RSA-OAEP
samlClientEncryptionDigestMethodHelp=Digest method to use when any RSA-OAEP algorithm is selected as the key transport algorithm. Default value SHA-256.
samlClientEncryptionMaskGenerationFunction=Mask generation function
samlClientEncryptionMaskGenerationFunctionHelp=Mask generation function to use when RSA-OAEP-11 is selected as the key transport algorithm. Default value mgf1sha256.
openIdVerifiableCredentials=OpenID for Verifiable Credentials
openIdVerifiableCredentialsHelp=This section is used to configure settings related to OpenID for Verifiable Credential Issuance (OID4VCI).
oid4vciEnabled=Enable OID4VCI
oid4vciEnabledHelp=Enable this option to allow the client to request verifiable credentials from Keycloak's OID4VCI credential endpoint.
noAccessPolicies=No access policies
noAccessPoliciesInstructions=There haven't been configured any access policies yet. Click the button below to configure the first policy.
noAccessPoliciesInstructions=There haven't been configured any access policies yet. Click the button below to configure the first policy.

View File

@@ -0,0 +1,167 @@
import { SelectControl } from "@keycloak/keycloak-ui-shared";
import { useEffect } from "react";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { convertAttributeNameToForm } from "../../util";
import { FormFields } from "../ClientDetails";
export const SamlEncryption = () => {
const { t } = useTranslation();
const { watch, setValue } = useFormContext();
const ALGORITHM_RSA_OAEP = "http://www.w3.org/2009/xmlenc11#rsa-oaep";
const ALGORITHM_RSA_OAEP_MGF1P =
"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p";
const keyEncryptionAlgorithm = watch(
convertAttributeNameToForm<FormFields>(
"attributes.saml.encryption.keyAlgorithm",
),
"",
);
// remove optional fields if not displayed
useEffect(() => {
if (keyEncryptionAlgorithm !== ALGORITHM_RSA_OAEP) {
setValue(
convertAttributeNameToForm<FormFields>(
"attributes.saml.encryption.maskGenerationFunction",
),
"",
);
if (keyEncryptionAlgorithm !== ALGORITHM_RSA_OAEP_MGF1P) {
setValue(
convertAttributeNameToForm<FormFields>(
"attributes.saml.encryption.digestMethod",
),
"",
);
}
}
}, [keyEncryptionAlgorithm]);
return (
<>
<SelectControl
name={convertAttributeNameToForm<FormFields>(
"attributes.saml.encryption.algorithm",
)}
label={t("samlClientEncryptionAlgorithm")}
labelIcon={t("samlClientEncryptionAlgorithmHelp")}
controller={{
defaultValue: "",
}}
options={[
{ key: "", value: t("choose") },
{
key: "http://www.w3.org/2009/xmlenc11#aes256-gcm",
value: "AES_256_GCM",
},
{
key: "http://www.w3.org/2009/xmlenc11#aes192-gcm",
value: "AES_192_GCM",
},
{
key: "http://www.w3.org/2009/xmlenc11#aes128-gcm",
value: "AES_128_GCM",
},
{
key: "http://www.w3.org/2001/04/xmlenc#aes256-cbc",
value: "AES_256_CBC",
},
{
key: "http://www.w3.org/2001/04/xmlenc#aes192-cbc",
value: "AES_192_CBC",
},
{
key: "http://www.w3.org/2001/04/xmlenc#aes128-cbc",
value: "AES_128_CBC",
},
]}
/>
<SelectControl
name={convertAttributeNameToForm<FormFields>(
"attributes.saml.encryption.keyAlgorithm",
)}
label={t("samlClientKeyEncryptionAlgorithm")}
labelIcon={t("samlClientKeyEncryptionAlgorithmHelp")}
controller={{
defaultValue: "",
}}
options={[
{ key: "", value: t("choose") },
{
key: ALGORITHM_RSA_OAEP,
value: "RSA-OAEP-11",
},
{
key: ALGORITHM_RSA_OAEP_MGF1P,
value: "RSA-OAEP-MGF1P",
},
{ key: "http://www.w3.org/2001/04/xmlenc#rsa-1_5", value: "RSA1_5" },
]}
/>
{(keyEncryptionAlgorithm === ALGORITHM_RSA_OAEP ||
keyEncryptionAlgorithm === ALGORITHM_RSA_OAEP_MGF1P) && (
<SelectControl
name={convertAttributeNameToForm<FormFields>(
"attributes.saml.encryption.digestMethod",
)}
label={t("samlClientEncryptionDigestMethod")}
labelIcon={t("samlClientEncryptionDigestMethodHelp")}
controller={{
defaultValue: "",
}}
options={[
{ key: "", value: t("choose") },
{
key: "http://www.w3.org/2001/04/xmlenc#sha512",
value: "SHA-512",
},
{
key: "http://www.w3.org/2001/04/xmlenc#sha256",
value: "SHA-256",
},
{ key: "http://www.w3.org/2000/09/xmldsig#sha1", value: "SHA-1" },
]}
/>
)}
{keyEncryptionAlgorithm === ALGORITHM_RSA_OAEP && (
<SelectControl
name={convertAttributeNameToForm<FormFields>(
"attributes.saml.encryption.maskGenerationFunction",
)}
label={t("samlClientEncryptionMaskGenerationFunction")}
labelIcon={t("samlClientEncryptionMaskGenerationFunctionHelp")}
controller={{
defaultValue: "",
}}
options={[
{ key: "", value: t("choose") },
{
key: "http://www.w3.org/2009/xmlenc11#mgf1sha512",
value: "mgf1sha512",
},
{
key: "http://www.w3.org/2009/xmlenc11#mgf1sha384",
value: "mgf1sha384",
},
{
key: "http://www.w3.org/2009/xmlenc11#mgf1sha256",
value: "mgf1sha256",
},
{
key: "http://www.w3.org/2009/xmlenc11#mgf1sha224",
value: "mgf1sha224",
},
{
key: "http://www.w3.org/2009/xmlenc11#mgf1sha1",
value: "mgf1sha1",
},
]}
/>
)}
</>
);
};

View File

@@ -5,6 +5,7 @@ import { FormAccess } from "../../components/form/FormAccess";
import { convertAttributeNameToForm } from "../../util";
import { FormFields } from "../ClientDetails";
import { Toggle } from "./SamlConfig";
import { SamlEncryption } from "./SamlEncryption";
export const SIGNATURE_ALGORITHMS = [
"RSA_SHA1",
@@ -45,6 +46,10 @@ export const SamlSignature = () => {
"attributes.saml.assertion.signature",
),
);
const samlEncryption = watch(
convertAttributeNameToForm<FormFields>("attributes.saml.encrypt"),
"false",
);
return (
<FormAccess
@@ -96,6 +101,7 @@ export const SamlSignature = () => {
value: name,
}))}
/>
{samlEncryption === "true" && <SamlEncryption />}
</>
)}
</FormAccess>

View File

@@ -38,19 +38,35 @@ type SamlKeysProps = {
save: () => void;
};
type KeyMapping = {
name: string;
title: string;
key: string;
relatedKeys: string[];
};
const KEYS = ["saml.signing", "saml.encryption"] as const;
export type KeyTypes = (typeof KEYS)[number];
const KEYS_MAPPING: { [key in KeyTypes]: { [index: string]: string } } = {
const KEYS_MAPPING: { [key in KeyTypes]: KeyMapping } = {
"saml.signing": {
name: convertAttributeNameToForm("attributes.saml.client.signature"),
title: "signingKeysConfig",
key: "clientSignature",
relatedKeys: [],
},
"saml.encryption": {
name: convertAttributeNameToForm("attributes.saml.encrypt"),
title: "encryptionKeysConfig",
key: "encryptAssertions",
relatedKeys: [
convertAttributeNameToForm("attributes.saml.encryption.algorithm"),
convertAttributeNameToForm("attributes.saml.encryption.keyAlgorithm"),
convertAttributeNameToForm("attributes.saml.encryption.digestMethod"),
convertAttributeNameToForm(
"attributes.saml.encryption.maskGenerationFunction",
),
],
},
};
@@ -215,6 +231,9 @@ export const SamlKeys = ({ clientId, save }: SamlKeysProps) => {
cancelButtonLabel: "no",
onConfirm: () => {
setValue(KEYS_MAPPING[selectedType!].name, "false");
for (const key of KEYS_MAPPING[selectedType!].relatedKeys) {
setValue(key, ""); // remove related attributes when disabled
}
save();
},
});
@@ -237,6 +256,9 @@ export const SamlKeys = ({ clientId, save }: SamlKeysProps) => {
attr={isChanged}
onClose={() => {
setIsChanged(undefined);
for (const key of KEYS_MAPPING[selectedType!].relatedKeys) {
setValue(key, ""); // take defaults when enabled
}
save();
setRefresh(refresh + 1);
}}
@@ -262,7 +284,10 @@ export const SamlKeys = ({ clientId, save }: SamlKeysProps) => {
clientId={clientId}
keyInfo={keyInfo?.[index]}
attr={attr}
onChanged={setIsChanged}
onChanged={(type) => {
setIsChanged(type);
setSelectedType(type);
}}
onGenerate={(type, isNew) => {
setSelectedType(type);
if (!isNew) {

View File

@@ -10,15 +10,29 @@ import { clickTableRowItem } from "../utils/table";
import { goToAdvancedTab, revertFineGrain, saveFineGrain } from "./advanced";
import {
assertCertificate,
assertEncryptionAlgorithm,
assertEncryptionKeyAlgorithm,
assertEncryptionDigestMethod,
assertEncryptionMaskGenerationFunction,
assertEncryptionAlgorithmInputVisible,
assertEncryptionKeyAlgorithmInputVisible,
assertEncryptionDigestMethodInputVisible,
assertEncryptionMaskGenerationFunctionInputVisible,
assertNameIdFormatDropdown,
assertSamlClientDetails,
assertTermsOfServiceUrl,
clickClientSignature,
clickEncryptionAssertions,
clickOffEncryptionAssertions,
clickGenerate,
clickPostBinding,
goToClientSettingsTab,
goToKeysTab,
saveSamlSettings,
selectEncryptionAlgorithmInput,
selectEncryptionKeyAlgorithmInput,
selectEncryptionDigestMethodInput,
selectEncryptionMaskGenerationFunctionInput,
setTermsOfServiceUrl,
} from "./saml";
@@ -120,6 +134,7 @@ test.describe("Clients SAML tests", () => {
});
test("should enable Encryption keys config", async ({ page }) => {
// enable encryption on keys tab
await goToKeysTab(page);
await clickEncryptionAssertions(page);
await clickGenerate(page);
@@ -129,12 +144,50 @@ test.describe("Clients SAML tests", () => {
);
await confirmModal(page);
await assertCertificate(page, false);
// assert encryption algorithms can be modified
await goToClientSettingsTab(page);
await assertEncryptionAlgorithm(page, "Choose...");
await assertEncryptionKeyAlgorithm(page, "Choose...");
await assertEncryptionDigestMethodInputVisible(page, false);
await assertEncryptionMaskGenerationFunctionInputVisible(page, false);
await selectEncryptionAlgorithmInput(page, "AES_256_GCM");
await selectEncryptionKeyAlgorithmInput(page, "RSA-OAEP-11");
await assertEncryptionDigestMethod(page, "Choose...");
await assertEncryptionMaskGenerationFunction(page, "Choose...");
await selectEncryptionDigestMethodInput(page, "SHA-256");
await selectEncryptionMaskGenerationFunctionInput(page, "mgf1sha256");
await selectEncryptionKeyAlgorithmInput(page, "RSA1_5");
await assertEncryptionDigestMethodInputVisible(page, false);
await assertEncryptionMaskGenerationFunctionInputVisible(page, false);
await selectEncryptionKeyAlgorithmInput(page, "RSA-OAEP-11");
await assertEncryptionDigestMethod(page, "Choose...");
await assertEncryptionMaskGenerationFunction(page, "Choose...");
await selectEncryptionDigestMethodInput(page, "SHA-256");
await selectEncryptionMaskGenerationFunctionInput(page, "mgf1sha256");
// disable encryption and check encryption algorithms are hidden
await goToKeysTab(page);
await clickOffEncryptionAssertions(page);
await confirmModal(page);
await goToClientSettingsTab(page);
await assertEncryptionAlgorithmInputVisible(page, false);
await assertEncryptionKeyAlgorithmInputVisible(page, false);
await assertEncryptionDigestMethodInputVisible(page, false);
await assertEncryptionMaskGenerationFunctionInputVisible(page, false);
// enable encryption again and check algorithms are empty/default
await goToKeysTab(page);
await clickEncryptionAssertions(page);
await confirmModal(page);
await goToClientSettingsTab(page);
await assertEncryptionAlgorithm(page, "Choose...");
await assertEncryptionKeyAlgorithm(page, "Choose...");
await assertEncryptionDigestMethodInputVisible(page, false);
await assertEncryptionMaskGenerationFunctionInputVisible(page, false);
});
test("should check SAML capabilities", async ({ page }) => {
// Assert Name ID Format dropdown exists
await assertNameIdFormatDropdown(page);
// Assert SAML Capabilities switches exist
const switches = [
['[data-testid="attributes.saml_force_name_id_format"]', "on"],
@@ -151,6 +204,9 @@ test.describe("Clients SAML tests", () => {
await switchOn(page, name);
}
}
// Assert Name ID Format dropdown exists
await assertNameIdFormatDropdown(page);
});
test("should check access settings", async ({ page }) => {

View File

@@ -1,10 +1,31 @@
import { Page, expect } from "@playwright/test";
import { selectItem, switchOff, switchOn } from "../utils/form";
import { Locator, Page, expect } from "@playwright/test";
import {
assertSelectValue,
selectItem,
switchOff,
switchOn,
} from "../utils/form";
function getTermsOfServiceUrl(page: Page) {
return page.getByTestId("attributes.tosUri");
}
function getKeyForEncryptionAlgorithmInput(page: Page) {
return page.locator("#attributes\\.saml🍺encryption🍺algorithm");
}
function getKeyForEncryptionKeyAlgorithmInput(page: Page) {
return page.locator("#attributes\\.saml🍺encryption🍺keyAlgorithm");
}
function getKeyForEncryptionDigestMethodInput(page: Page) {
return page.locator("#attributes\\.saml🍺encryption🍺digestMethod");
}
function getKeyForEncryptionMaskGenerationFunctionInput(page: Page) {
return page.locator("#attributes\\.saml🍺encryption🍺maskGenerationFunction");
}
export async function setTermsOfServiceUrl(page: Page, url: string) {
await getTermsOfServiceUrl(page).fill(url);
}
@@ -37,6 +58,10 @@ export async function goToKeysTab(page: Page) {
await page.getByTestId("keysTab").click();
}
export async function goToClientSettingsTab(page: Page) {
await page.getByTestId("clientSettingsTab").click();
}
export async function clickClientSignature(page: Page) {
await switchOff(page, "#clientSignature");
}
@@ -49,6 +74,10 @@ export async function clickEncryptionAssertions(page: Page) {
await switchOn(page, "#encryptAssertions");
}
export async function clickOffEncryptionAssertions(page: Page) {
await switchOff(page, "#encryptAssertions");
}
export async function clickGenerate(page: Page) {
await page.getByTestId("generate").click();
}
@@ -66,3 +95,96 @@ export async function assertNameIdFormatDropdown(page: Page) {
);
}
}
export async function selectEncryptionAlgorithmInput(
page: Page,
value: string,
) {
await selectItem(page, getKeyForEncryptionAlgorithmInput(page), value);
}
export async function selectEncryptionKeyAlgorithmInput(
page: Page,
value: string,
) {
await selectItem(page, getKeyForEncryptionKeyAlgorithmInput(page), value);
}
export async function selectEncryptionDigestMethodInput(
page: Page,
value: string,
) {
await selectItem(page, getKeyForEncryptionDigestMethodInput(page), value);
}
export async function selectEncryptionMaskGenerationFunctionInput(
page: Page,
value: string,
) {
await selectItem(
page,
getKeyForEncryptionMaskGenerationFunctionInput(page),
value,
);
}
export async function assertEncryptionAlgorithm(page: Page, value: string) {
await assertSelectValue(getKeyForEncryptionAlgorithmInput(page), value);
}
export async function assertEncryptionKeyAlgorithm(page: Page, value: string) {
await assertSelectValue(getKeyForEncryptionKeyAlgorithmInput(page), value);
}
export async function assertEncryptionDigestMethod(page: Page, value: string) {
await assertSelectValue(getKeyForEncryptionDigestMethodInput(page), value);
}
export async function assertEncryptionMaskGenerationFunction(
page: Page,
value: string,
) {
await assertSelectValue(
getKeyForEncryptionMaskGenerationFunctionInput(page),
value,
);
}
async function assertInputVisible(locator: Locator, visible: boolean) {
if (visible) {
await expect(locator).toBeVisible();
} else {
await expect(locator).toBeHidden();
}
}
export async function assertEncryptionAlgorithmInputVisible(
page: Page,
visible: boolean,
) {
await assertInputVisible(getKeyForEncryptionAlgorithmInput(page), visible);
}
export async function assertEncryptionKeyAlgorithmInputVisible(
page: Page,
visible: boolean,
) {
await assertInputVisible(getKeyForEncryptionKeyAlgorithmInput(page), visible);
}
export async function assertEncryptionDigestMethodInputVisible(
page: Page,
visible: boolean,
) {
await assertInputVisible(getKeyForEncryptionDigestMethodInput(page), visible);
}
export async function assertEncryptionMaskGenerationFunctionInputVisible(
page: Page,
visible: boolean,
) {
await assertInputVisible(
getKeyForEncryptionMaskGenerationFunctionInput(page),
visible,
);
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.connections.jpa.updater.liquibase.custom;
import liquibase.exception.CustomChangeException;
import liquibase.statement.SqlStatement;
import liquibase.statement.core.RawParameterizedSqlStatement;
/**
*
* @author rmartinc
*/
public class JpaUpdate26_4_0_SamlEncryptionAttributes extends CustomKeycloakTask {
@Override
protected String getTaskId() {
return "Insert legacy encryption attributes in SAML clients";
}
@Override
protected void generateStatementsImpl() throws CustomChangeException {
statements.add(createInsertQueryForAttribute("saml.encryption.algorithm", "http://www.w3.org/2001/04/xmlenc#aes128-cbc"));
statements.add(createInsertQueryForAttribute("saml.encryption.keyAlgorithm", "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"));
statements.add(createInsertQueryForAttribute("saml.encryption.digestMethod", "http://www.w3.org/2000/09/xmldsig#sha1"));
}
private SqlStatement createInsertQueryForAttribute(String attribute, String value) {
final String clientTable = getTableName("CLIENT");
final String clientAttributesTable = getTableName("CLIENT_ATTRIBUTES");
return new RawParameterizedSqlStatement(
"INSERT INTO " + clientAttributesTable + " (CLIENT_ID,NAME,VALUE) " +
"SELECT ID, ?, ? FROM " + clientTable + " WHERE PROTOCOL = ? AND ID NOT IN " +
"(SELECT CLIENT_ID FROM " + clientAttributesTable + " WHERE NAME = ?)",
attribute, value, "saml", attribute
);
}
}

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
~ * Copyright 2025 Red Hat, Inc. and/or its affiliates
~ * and other contributors as indicated by the @author tags.
~ *
~ * Licensed under the Apache License, Version 2.0 (the "License");
~ * you may not use this file except in compliance with the License.
~ * You may obtain a copy of the License at
~ *
~ * http://www.apache.org/licenses/LICENSE-2.0
~ *
~ * Unless required by applicable law or agreed to in writing, software
~ * distributed under the License is distributed on an "AS IS" BASIS,
~ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ * See the License for the specific language governing permissions and
~ * limitations under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="keycloak" id="26.4.0-40933-saml-encryption-attributes">
<customChange class="org.keycloak.connections.jpa.updater.liquibase.custom.JpaUpdate26_4_0_SamlEncryptionAttributes"/>
</changeSet>
</databaseChangeLog>

View File

@@ -88,5 +88,6 @@
<include file="META-INF/jpa-changelog-26.2.0.xml"/>
<include file="META-INF/jpa-changelog-26.2.6.xml"/>
<include file="META-INF/jpa-changelog-26.3.0.xml"/>
<include file="META-INF/jpa-changelog-26.4.0.xml"/>
</databaseChangeLog>

View File

@@ -30,6 +30,7 @@ import org.keycloak.exportimport.ExportOptions;
import org.keycloak.exportimport.util.ExportUtils;
import org.keycloak.keys.KeyProvider;
import org.keycloak.migration.MigrationProvider;
import org.keycloak.migration.ModelVersion;
import org.keycloak.migration.migrators.MigrateTo8_0_0;
import org.keycloak.migration.migrators.MigrationUtils;
import org.keycloak.models.AuthenticationExecutionModel;
@@ -193,6 +194,8 @@ public class DefaultExportImportManager implements ExportImportManager {
@Override
public void importRealm(RealmRepresentation rep, RealmModel newRealm, Runnable userImport) {
ModelVersion version = DefaultMigrationManager.getModelVersionFromRep(rep);
convertDeprecatedSocialProviders(rep);
convertDeprecatedApplications(session, rep);
convertDeprecatedClientTemplates(rep);
@@ -393,7 +396,7 @@ public class DefaultExportImportManager implements ExportImportManager {
Map<String, ClientModel> createdClients = new HashMap<>();
if (rep.getClients() != null) {
createdClients = createClients(session, rep, newRealm, mappedFlows);
createdClients = createClients(session, version, rep, newRealm, mappedFlows);
}
importRoles(rep.getRoles(), newRealm);
@@ -561,8 +564,17 @@ public class DefaultExportImportManager implements ExportImportManager {
return role;
}
private static Map<String, ClientModel> createClients(KeycloakSession session, RealmRepresentation rep, RealmModel realm, Map<String, String> mappedFlows) {
private static void addSamlEncryptionAttributes(ClientModel app) {
if ("saml".equals(app.getProtocol()) && "true".equals(app.getAttribute("saml.encrypt"))) {
app.setAttribute("saml.encryption.algorithm", "http://www.w3.org/2001/04/xmlenc#aes128-cbc");
app.setAttribute("saml.encryption.keyAlgorithm", "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p");
app.setAttribute("saml.encryption.digestMethod", "http://www.w3.org/2000/09/xmldsig#sha1");
}
}
private static Map<String, ClientModel> createClients(KeycloakSession session, ModelVersion version, RealmRepresentation rep, RealmModel realm, Map<String, String> mappedFlows) {
Map<String, ClientModel> appMap = new HashMap<>();
final boolean samlEncryptionAttributes = version != null && version.lessThan(new ModelVersion(26, 4, 0));
for (ClientRepresentation resourceRep : rep.getClients()) {
if (Profile.isFeatureEnabled(Feature.ADMIN_FINE_GRAINED_AUTHZ_V2)) {
if (realm.getAdminPermissionsClient() != null && realm.getAdminPermissionsClient().getClientId().equals(resourceRep.getClientId())) {
@@ -574,6 +586,9 @@ public class DefaultExportImportManager implements ExportImportManager {
if (postLogoutRedirectUris == null) {
app.setAttribute(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS, "+");
}
if (samlEncryptionAttributes) {
addSamlEncryptionAttributes(app);
}
appMap.put(app.getClientId(), app);
ValidationUtil.validateClient(session, app, false, r -> {

View File

@@ -196,13 +196,7 @@ public class DefaultMigrationManager implements MigrationManager {
@Override
public void migrate(RealmModel realm, RealmRepresentation rep, boolean skipUserDependent) {
ModelVersion stored = null;
if (rep.getKeycloakVersion() != null) {
stored = convertRHSSOVersionToKeycloakVersion(rep.getKeycloakVersion());
if (stored == null) {
stored = new ModelVersion(rep.getKeycloakVersion());
}
}
ModelVersion stored = getModelVersionFromRep(rep);
if (stored == null) {
stored = migrations[0].getVersion();
} else {
@@ -241,4 +235,15 @@ public class DefaultMigrationManager implements MigrationManager {
return null;
}
public static ModelVersion getModelVersionFromRep(RealmRepresentation rep) {
ModelVersion version = null;
if (rep.getKeycloakVersion() != null) {
version = convertRHSSOVersionToKeycloakVersion(rep.getKeycloakVersion());
if (version == null) {
version = new ModelVersion(rep.getKeycloakVersion());
}
}
return version;
}
}

View File

@@ -134,6 +134,7 @@ public enum JBossSAMLURIConstants {
XMLSCHEMA_NSURI("http://www.w3.org/2001/XMLSchema"),
XMLDSIG_NSURI("http://www.w3.org/2000/09/xmldsig#"),
XMLENC_NSURI("http://www.w3.org/2001/04/xmlenc#"),
XMLENC11_NSURI("http://www.w3.org/2009/xmlenc11#"),
XSI_NSURI("http://www.w3.org/2001/XMLSchema-instance"),
;

View File

@@ -17,6 +17,8 @@
package org.keycloak.saml;
import org.apache.xml.security.encryption.XMLCipher;
import org.jboss.logging.Logger;
import org.keycloak.common.util.KeycloakUriBuilder;
@@ -68,9 +70,9 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
protected boolean signAssertions;
protected SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSA_SHA1;
protected String relayState;
protected int encryptionKeySize = 128;
protected int encryptionKeySize = 256;
protected PublicKey encryptionPublicKey;
protected String encryptionAlgorithm = "AES";
protected String encryptionAlgorithm = XMLCipher.AES_256_GCM;
protected boolean encrypt;
protected String canonicalizationMethodType = CanonicalizationMethod.EXCLUSIVE;
protected String keyEncryptionAlgorithm;
@@ -129,12 +131,42 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
return (T)this;
}
private String updateAesWithSize(int size) {
switch (size) {
case 128: return XMLCipher.AES_128;
case 192: return XMLCipher.AES_192;
case 256: return XMLCipher.AES_256;
default:
throw new RuntimeException("Invalid size for AES: " + size);
}
}
/**
* The encryption algorithm to use as expected by XMLCipher (XMLCipher.AES_256_GCM for example)
* @param alg The algorithm in XMLCipher format.
* @return This same builder
*/
public T encryptionAlgorithm(String alg) {
this.encryptionAlgorithm = alg;
if ("AES".equals(alg)) {
// deprecated way, remove later
this.encryptionAlgorithm = updateAesWithSize(this.encryptionKeySize);
} else {
this.encryptionAlgorithm = alg;
}
return (T)this;
}
/**
* Sets the size for the AES method to use.
* @param size
* @return This same builder
* @deprecated Use directly the XMLCipher algorithm in the
* {@link #encryptionAlgorithm(java.lang.String)} method
* (for example XMLCipher.AES_256_GCM).
*/
@Deprecated(since = "26.4.0", forRemoval = true)
public T encryptionKeySize(int size) {
this.encryptionAlgorithm = updateAesWithSize(size);
this.encryptionKeySize = size;
return (T)this;
}
@@ -284,13 +316,15 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
QName encryptedAssertionElementQName = new QName(JBossSAMLURIConstants.ASSERTION_NSURI.get(),
JBossSAMLConstants.ENCRYPTED_ASSERTION.get(), samlNSPrefix);
byte[] secret = RandomSecret.createRandomSecret(encryptionKeySize / 8);
SecretKey secretKey = new SecretKeySpec(secret, encryptionAlgorithm);
final String keyAlgorithm = XMLEncryptionUtil.getJCEKeyAlgorithmFromURI(encryptionAlgorithm);
final int keySize = XMLEncryptionUtil.getKeyLengthFromURI(encryptionAlgorithm);
byte[] secret = RandomSecret.createRandomSecret(keySize / 8);
SecretKey secretKey = new SecretKeySpec(secret, keyAlgorithm);
// encrypt the Assertion element and replace it with a EncryptedAssertion element.
XMLEncryptionUtil.encryptElement(new QName(JBossSAMLURIConstants.ASSERTION_NSURI.get(),
JBossSAMLConstants.ASSERTION.get(), samlNSPrefix), samlDocument, encryptionPublicKey, secretKey, encryptionKeySize,
encryptedAssertionElementQName, true, keyEncryptionAlgorithm, keyEncryptionDigestMethod, keyEncryptionMgfAlgorithm);
JBossSAMLConstants.ASSERTION.get(), samlNSPrefix), samlDocument, encryptionPublicKey, secretKey, keySize,
encryptedAssertionElementQName, true, encryptionAlgorithm, keyEncryptionAlgorithm, keyEncryptionDigestMethod, keyEncryptionMgfAlgorithm);
} catch (Exception e) {
throw new ProcessingException("failed to encrypt", e);
}

View File

@@ -16,6 +16,7 @@
*/
package org.keycloak.saml.processing.core.util;
import org.apache.xml.security.algorithms.JCEMapper;
import org.apache.xml.security.encryption.EncryptedData;
import org.apache.xml.security.encryption.EncryptedKey;
import org.apache.xml.security.encryption.XMLCipher;
@@ -75,9 +76,7 @@ public class XMLEncryptionUtil {
public static final String DS_KEY_INFO = "ds:KeyInfo";
private static final String RSA_ENCRYPTION_SCHEME = Objects.equals(System.getProperty("keycloak.saml.key_trans.rsa_v1.5"), "true")
? XMLCipher.RSA_v1dot5
: XMLCipher.RSA_OAEP;
private static final String RSA_ENCRYPTION_SCHEME = XMLCipher.RSA_OAEP_11;
/**
* <p>
@@ -93,14 +92,13 @@ public class XMLEncryptionUtil {
* @param document
* @param keyToBeEncrypted Symmetric Key (SecretKey)
* @param keyUsedToEncryptSecretKey Asymmetric Key (Public Key)
* @param keySize Length of the key
*
* @return
*
* @throws org.keycloak.saml.common.exceptions.ProcessingException
*/
private static EncryptedKey encryptKey(Document document, SecretKey keyToBeEncrypted, PublicKey keyUsedToEncryptSecretKey,
int keySize, String keyEncryptionAlgorithm, String keyEncryptionDigestMethod,
String keyEncryptionAlgorithm, String keyEncryptionDigestMethod,
String keyEncryptionMgfAlgorithm) throws ProcessingException {
XMLCipher keyCipher;
@@ -114,17 +112,32 @@ public class XMLEncryptionUtil {
}
}
public static String getJCEKeyAlgorithmFromURI(String algorithm) {
return JCEMapper.getJCEKeyAlgorithmFromURI(algorithm);
}
public static int getKeyLengthFromURI(String algorithm) {
return JCEMapper.getKeyLengthFromURI(algorithm);
}
public static void encryptElement(QName elementQName, Document document, PublicKey publicKey, SecretKey secretKey,
int keySize, QName wrappingElementQName, boolean addEncryptedKeyInKeyInfo) throws ProcessingException {
encryptElement(elementQName, document, publicKey, secretKey, keySize, wrappingElementQName, addEncryptedKeyInKeyInfo,
null, null, null);
null, null, null, null);
}
public static void encryptElement(QName elementQName, Document document, PublicKey publicKey, SecretKey secretKey,
int keySize, QName wrappingElementQName, boolean addEncryptedKeyInKeyInfo,
String keyEncryptionAlgorithm) throws ProcessingException {
encryptElement(elementQName, document, publicKey, secretKey, keySize, wrappingElementQName,
addEncryptedKeyInKeyInfo, keyEncryptionAlgorithm, null, null);
addEncryptedKeyInKeyInfo, null, keyEncryptionAlgorithm, null, null);
}
public static void encryptElement(QName elementQName, Document document, PublicKey publicKey, SecretKey secretKey,
int keySize, QName wrappingElementQName, boolean addEncryptedKeyInKeyInfo, String keyEncryptionAlgorithm,
String keyEncryptionDigestMethod, String keyEncryptionMgfAlgorithm) throws ProcessingException {
encryptElement(elementQName, document, publicKey, secretKey, keySize, wrappingElementQName, addEncryptedKeyInKeyInfo,
null, keyEncryptionAlgorithm, keyEncryptionDigestMethod, keyEncryptionMgfAlgorithm);
}
/**
@@ -138,6 +151,7 @@ public class XMLEncryptionUtil {
* @param keySize The size of the public key
* @param wrappingElementQName A QName of an element that will wrap the encrypted element
* @param addEncryptedKeyInKeyInfo Need for the EncryptedKey to be placed in ds:KeyInfo
* @param encryptionAlgorithm The encryption algorithm
* @param keyEncryptionAlgorithm The wrap algorithm for the secret key (can be null, default is used depending the publicKey type)
* @param keyEncryptionDigestMethod An optional digestMethod to use (can be null)
* @param keyEncryptionMgfAlgorithm The xenc11 MGF Algorithm to use (can be null)
@@ -145,8 +159,8 @@ public class XMLEncryptionUtil {
* @throws ProcessingException
*/
public static void encryptElement(QName elementQName, Document document, PublicKey publicKey, SecretKey secretKey,
int keySize, QName wrappingElementQName, boolean addEncryptedKeyInKeyInfo, String keyEncryptionAlgorithm,
String keyEncryptionDigestMethod, String keyEncryptionMgfAlgorithm) throws ProcessingException {
int keySize, QName wrappingElementQName, boolean addEncryptedKeyInKeyInfo, String encryptionAlgorithm,
String keyEncryptionAlgorithm, String keyEncryptionDigestMethod, String keyEncryptionMgfAlgorithm) throws ProcessingException {
if (elementQName == null)
throw logger.nullArgumentError("elementQName");
if (document == null)
@@ -160,14 +174,40 @@ public class XMLEncryptionUtil {
if (documentElement == null)
throw logger.domMissingDocElementError(elementQName.toString());
XMLCipher cipher = null;
// set default algorithms
if (encryptionAlgorithm == null) {
// set default encryption based on the secret key passed
encryptionAlgorithm = getXMLEncryptionURL(secretKey.getAlgorithm(), keySize);
}
if (keyEncryptionAlgorithm == null) {
// get default one for the public key
keyEncryptionAlgorithm = getXMLEncryptionURLForKeyUnwrap(publicKey.getAlgorithm(), keySize);
}
EncryptedKey encryptedKey = encryptKey(document, secretKey, publicKey, keySize, keyEncryptionAlgorithm, keyEncryptionDigestMethod, keyEncryptionMgfAlgorithm);
String encryptionAlgorithm = getXMLEncryptionURL(secretKey.getAlgorithm(), keySize);
if ((XMLCipher.RSA_OAEP.equals(keyEncryptionAlgorithm) || XMLCipher.RSA_OAEP_11.equals(keyEncryptionAlgorithm))) {
if (keyEncryptionDigestMethod == null) {
keyEncryptionDigestMethod = XMLCipher.SHA256; // default digest method to SHA256
} else if (XMLCipher.SHA1.equals(keyEncryptionDigestMethod)){
keyEncryptionDigestMethod = null; // default by spec
}
} else {
keyEncryptionDigestMethod = null; // not used for RSA_v1dot5
}
if (XMLCipher.RSA_OAEP_11.equals(keyEncryptionAlgorithm)) {
if (keyEncryptionMgfAlgorithm == null) {
keyEncryptionMgfAlgorithm = EncryptionConstants.MGF1_SHA256; // default mgf to mgf1sha256
} else if (EncryptionConstants.MGF1_SHA1.equals(keyEncryptionMgfAlgorithm)) {
keyEncryptionMgfAlgorithm = null; // default by spec
}
} else {
keyEncryptionMgfAlgorithm = null; // only available for RSA_OAEP_11
}
EncryptedKey encryptedKey = encryptKey(document, secretKey, publicKey, keyEncryptionAlgorithm, keyEncryptionDigestMethod, keyEncryptionMgfAlgorithm);
XMLCipher cipher = null;
// Encrypt the Document
try {
cipher = XMLCipher.getInstance(encryptionAlgorithm);

View File

@@ -213,7 +213,6 @@ public class SamlClient extends ClientConfigResolver {
public void setClientEncryptingCertificate(String val) {
client.setAttribute(SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE, val);
}
public String getClientEncryptingPrivateKey() {
@@ -222,7 +221,38 @@ public class SamlClient extends ClientConfigResolver {
public void setClientEncryptingPrivateKey(String val) {
client.setAttribute(SamlConfigAttributes.SAML_ENCRYPTION_PRIVATE_KEY_ATTRIBUTE, val);
}
public String getClientEncryptingAlgorithm() {
return client.getAttribute(SamlConfigAttributes.SAML_ENCRYPTION_ALGORITHM);
}
public void setClientEncryptingAlgorithm(String val) {
client.setAttribute(SamlConfigAttributes.SAML_ENCRYPTION_ALGORITHM, val);
}
public String getClientEncryptingKeyAlgorithm() {
return client.getAttribute(SamlConfigAttributes.SAML_ENCRYPTION_KEY_ALGORITHM);
}
public void setClientEncryptingKeyAlgorithm(String val) {
client.setAttribute(SamlConfigAttributes.SAML_ENCRYPTION_KEY_ALGORITHM, val);
}
public String getClientEncryptingDigestMethod() {
return client.getAttribute(SamlConfigAttributes.SAML_ENCRYPTION_DIGEST_METHOD);
}
public void setClientEncryptingDigestMethod(String val) {
client.setAttribute(SamlConfigAttributes.SAML_ENCRYPTION_DIGEST_METHOD, val);
}
public String getClientEncryptingMaskGenerationFunction() {
return client.getAttribute(SamlConfigAttributes.SAML_ENCRYPTION_MASK_GENERATION_FUNTION);
}
public void setClientEncryptingMaskGenerationFunction(String val) {
client.setAttribute(SamlConfigAttributes.SAML_ENCRYPTION_MASK_GENERATION_FUNTION, val);
}
/**

View File

@@ -42,6 +42,10 @@ public interface SamlConfigAttributes {
String SAML_SIGNING_CERTIFICATE_ATTRIBUTE = "saml.signing." + CertificateInfoHelper.X509CERTIFICATE;
String SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE = "saml.encryption." + CertificateInfoHelper.X509CERTIFICATE;
String SAML_ENCRYPTION_PRIVATE_KEY_ATTRIBUTE = "saml.encryption." + CertificateInfoHelper.PRIVATE_KEY;
String SAML_ENCRYPTION_ALGORITHM = "saml.encryption.algorithm";
String SAML_ENCRYPTION_KEY_ALGORITHM = "saml.encryption.keyAlgorithm";
String SAML_ENCRYPTION_DIGEST_METHOD = "saml.encryption.digestMethod";
String SAML_ENCRYPTION_MASK_GENERATION_FUNTION = "saml.encryption.maskGenerationFunction";
String SAML_ASSERTION_LIFESPAN = "saml.assertion.lifespan";
String SAML_ARTIFACT_BINDING_IDENTIFIER = "saml.artifact.binding.identifier";
String SAML_ALLOW_ECP_FLOW = "saml.allow.ecp.flow";

View File

@@ -597,14 +597,12 @@ public class SamlProtocol implements LoginProtocol {
}
if (samlClient.requiresEncryption()) {
PublicKey publicKey = null;
try {
publicKey = SamlProtocolUtils.getEncryptionKey(client);
SamlProtocolUtils.setupEncryption(samlClient, bindingBuilder);
} catch (Exception e) {
logger.error("failed", e);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.FAILED_TO_PROCESS_RESPONSE);
}
bindingBuilder.encrypt(publicKey);
}
try {
samlDocument = builder.buildDocument(samlModel);

View File

@@ -39,8 +39,8 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.saml.SignatureAlgorithm;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants;
import org.keycloak.saml.validators.DestinationValidator;
import javax.xml.crypto.dsig.CanonicalizationMethod;
import java.util.ArrayList;
import java.util.HashMap;

View File

@@ -26,6 +26,7 @@ import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.UriInfo;
import org.apache.xml.security.encryption.XMLCipher;
import org.jboss.logging.Logger;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.PemUtils;
@@ -40,6 +41,7 @@ import org.keycloak.dom.saml.v2.protocol.StatusType;
import org.keycloak.models.ClientModel;
import org.keycloak.rotation.HardcodedKeyLocator;
import org.keycloak.rotation.KeyLocator;
import org.keycloak.saml.BaseSAML2BindingBuilder;
import org.keycloak.saml.SignatureAlgorithm;
import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
@@ -121,6 +123,28 @@ public class SamlProtocolUtils {
return getPublicKey(client, SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE);
}
public static void setupEncryption(SamlClient samlClient, BaseSAML2BindingBuilder<?> bindingBuilder) throws VerificationException {
PublicKey publicKey = getPublicKey(samlClient.getClientEncryptingCertificate());
bindingBuilder.encrypt(publicKey);
if (samlClient.getClientEncryptingAlgorithm() != null) {
bindingBuilder.encryptionAlgorithm(samlClient.getClientEncryptingAlgorithm());
}
if (samlClient.getClientEncryptingKeyAlgorithm() != null) {
bindingBuilder.keyEncryptionAlgorithm(samlClient.getClientEncryptingKeyAlgorithm());
}
if (samlClient.getClientEncryptingDigestMethod() != null &&
(XMLCipher.RSA_OAEP.equals(samlClient.getClientEncryptingKeyAlgorithm()) ||
XMLCipher.RSA_OAEP_11.equals(samlClient.getClientEncryptingKeyAlgorithm()))) {
// digest method is only available to rsa oaep
bindingBuilder.keyEncryptionDigestMethod(samlClient.getClientEncryptingDigestMethod());
}
if (samlClient.getClientEncryptingMaskGenerationFunction() != null &&
XMLCipher.RSA_OAEP_11.equals(samlClient.getClientEncryptingKeyAlgorithm())) {
// the mgf is only available for rsa oaep 11
bindingBuilder.keyEncryptionMgfAlgorithm(samlClient.getClientEncryptingMaskGenerationFunction());
}
}
public static PublicKey getPublicKey(ClientModel client, String attribute) throws VerificationException {
String certPem = client.getAttribute(attribute);
return getPublicKey(certPem);

View File

@@ -1305,14 +1305,12 @@ public class SamlService extends AuthorizationEndpointBase {
// Encrypt assertion if client requires it
if (samlClient.requiresEncryption()) {
PublicKey publicKey = null;
try {
publicKey = SamlProtocolUtils.getEncryptionKey(clientModel);
SamlProtocolUtils.setupEncryption(samlClient, bindingBuilder);
} catch (Exception e) {
logger.error("Failed to obtain encryption key for client", e);
return emptyArtifactResponseMessage(artifactResolveMessage, null);
}
bindingBuilder.encrypt(publicKey);
}
}

View File

@@ -178,7 +178,12 @@ public class SamlEncryptionTest {
@Test
public void testAES256() throws Exception {
testEncryption(rsaKeyPair, "AES", 256, null, null, null);
testEncryption(rsaKeyPair, XMLCipher.AES_256, -1, null, null, null);
}
@Test
public void testAES256GCM() throws Exception {
testEncryption(rsaKeyPair, XMLCipher.AES_256_GCM, -1, null, null, null);
}
@Test

View File

@@ -18,6 +18,7 @@ package org.keycloak.testsuite.migration;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.xml.security.encryption.XMLCipher;
import org.hamcrest.Matchers;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.ClientResource;
@@ -48,6 +49,7 @@ import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.protocol.saml.SamlProtocolFactory;
import org.keycloak.protocol.saml.util.ArtifactBindingUtils;
import org.keycloak.representations.AccessToken;
@@ -149,7 +151,8 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
protected void testMigratedMigrationData(boolean supportsAuthzService) {
assertNames(migrationRealm.roles().list(), "offline_access", "uma_authorization", "default-roles-migration", "migration-test-realm-role");
List<String> expectedClientIds = new ArrayList<>(Arrays.asList("account", "account-console", "admin-cli", "broker", "migration-test-client", "migration-saml-client", "realm-management", "security-admin-console"));
List<String> expectedClientIds = new ArrayList<>(Arrays.asList("account", "account-console", "admin-cli", "broker", "migration-test-client", "migration-saml-client",
"realm-management", "security-admin-console", "http://localhost:8280/sales-post-enc/"));
if (supportsAuthzService) {
expectedClientIds.add("authz-servlet");
@@ -449,6 +452,10 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
testIdpLinkActionAvailable(migrationRealm);
}
protected void testMigrationTo26_4_0() {
testSamlEncryptionAttributes(migrationRealm);
}
private void testClientContainsExpectedClientScopes() {
// Test OIDC client contains expected client scopes
ClientResource migrationTestOIDCClient = ApiUtil.findClientByClientId(migrationRealm, "migration-test-client");
@@ -1381,4 +1388,19 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
assertTrue(clientRepresentation.isFullScopeAllowed());
assertTrue(Boolean.parseBoolean(clientRepresentation.getAttributes().get(Constants.USE_LIGHTWEIGHT_ACCESS_TOKEN_ENABLED)));
}
private void testSamlEncryptionAttributes(RealmResource realm) {
// check all the saml clients have the encryption attributes
List<ClientRepresentation> samlClients = realm.clients().findAll().stream()
.filter(client -> SamlProtocol.LOGIN_PROTOCOL.equals(client.getProtocol()))
.filter(client -> "true".equals(client.getAttributes().get(SamlConfigAttributes.SAML_ENCRYPT)))
.collect(Collectors.toList());
assertThat(samlClients.size(), is(1));
for (ClientRepresentation client : samlClients) {
assertThat(client.getAttributes().get(SamlConfigAttributes.SAML_ENCRYPTION_ALGORITHM), is(XMLCipher.AES_128));
assertThat(client.getAttributes().get(SamlConfigAttributes.SAML_ENCRYPTION_KEY_ALGORITHM), is(XMLCipher.RSA_OAEP));
assertThat(client.getAttributes().get(SamlConfigAttributes.SAML_ENCRYPTION_DIGEST_METHOD), is(XMLCipher.SHA1));
assertThat(client.getAttributes().get(SamlConfigAttributes.SAML_ENCRYPTION_MASK_GENERATION_FUNTION), nullValue());
}
}
}

View File

@@ -82,6 +82,7 @@ public class JsonFileImport198MigrationTest extends AbstractJsonFileImportMigrat
testMigrationTo25_0_0();
testMigrationTo26_0_0(false);
testMigrationTo26_3_0();
testMigrationTo26_4_0();
}
@Override

View File

@@ -74,6 +74,7 @@ public class MigrationTest extends AbstractMigrationTest {
testMigrationTo26_0_0(true);
testMigrationTo26_1_0(true);
testMigrationTo26_3_0();
testMigrationTo26_4_0();
}
@Test
@@ -89,5 +90,6 @@ public class MigrationTest extends AbstractMigrationTest {
testMigrationTo26_0_0(true);
testMigrationTo26_1_0(true);
testMigrationTo26_3_0();
testMigrationTo26_4_0();
}
}

View File

@@ -1,9 +1,14 @@
package org.keycloak.testsuite.saml;
import org.junit.Test;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.common.util.PemUtils;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.protocol.saml.SamlProtocolUtils;
import org.keycloak.rotation.HardcodedKeyLocator;
import org.keycloak.saml.SignatureAlgorithm;
import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
@@ -13,6 +18,7 @@ import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
import org.keycloak.saml.processing.web.util.RedirectBindingUtil;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
@@ -23,6 +29,8 @@ import org.keycloak.testsuite.util.SamlClient.Binding;
import org.keycloak.testsuite.util.SamlClient.RedirectStrategyWithSwitchableFollowRedirect;
import org.keycloak.testsuite.util.SamlClient.Step;
import org.keycloak.testsuite.util.SamlClientBuilder;
import org.keycloak.testsuite.util.SamlUtils;
import org.keycloak.utils.StringUtil;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
@@ -40,11 +48,15 @@ import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.apache.xml.security.encryption.EncryptedData;
import org.apache.xml.security.encryption.XMLCipher;
import org.apache.xml.security.utils.EncryptionConstants;
import org.hamcrest.Matcher;
import org.jboss.resteasy.util.Encode;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.Matchers.containsString;
@@ -54,6 +66,7 @@ import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.matchesRegex;
import static org.junit.Assert.assertTrue;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.PROTOCOL_NSURI;
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_PORT;
@@ -371,4 +384,106 @@ public class BasicSamlTest extends AbstractSamlTest {
assertThat(action, endsWith("javascript&colon;alert('xss');"));
}
}
private void testEncryption(String algorithm, String keyAlgorithm, String digestMethod, String maskGenerationFunction) throws Exception {
testEncryption(algorithm, keyAlgorithm, digestMethod, maskGenerationFunction,
algorithm, keyAlgorithm, digestMethod, maskGenerationFunction);
}
private void testEncryption(String algorithm, String keyAlgorithm, String digestMethod, String maskGenerationFunction,
String expectedAlgorithm, String expectedKeyAlgorithm, String expectedDigestMethod, String expectedMaxskGenerationFuntion) throws Exception {
try (var c = ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST_ENC)
.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, Boolean.TRUE.toString())
.setAttribute(SamlConfigAttributes.SAML_ENCRYPT, Boolean.TRUE.toString())
.setAttribute(SamlConfigAttributes.SAML_ENCRYPTION_ALGORITHM, algorithm)
.setAttribute(SamlConfigAttributes.SAML_ENCRYPTION_KEY_ALGORITHM, keyAlgorithm)
.setAttribute(SamlConfigAttributes.SAML_ENCRYPTION_DIGEST_METHOD, digestMethod)
.setAttribute(SamlConfigAttributes.SAML_ENCRYPTION_MASK_GENERATION_FUNTION, maskGenerationFunction)
.update()) {
SAMLDocumentHolder holder = new SamlClientBuilder()
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_ENC, SAML_ASSERTION_CONSUMER_URL_SALES_POST_ENC, Binding.POST)
.signWith(SAML_CLIENT_SALES_POST_ENC_PRIVATE_KEY, SAML_CLIENT_SALES_POST_ENC_PUBLIC_KEY)
.build()
.login().user(bburkeUser).build()
.getSamlResponse(Binding.POST);
// check it is signed
SamlProtocolUtils.verifyDocumentSignature(holder.getSamlDocument(), new HardcodedKeyLocator(PemUtils.decodePublicKey(REALM_PUBLIC_KEY)));
// check document is encrypted
ResponseType responseType = (ResponseType) holder.getSamlObject();
assertTrue("Assertion is not encrypted", AssertionUtil.isAssertionEncrypted(responseType));
SamlDeployment deployment = SamlUtils.getSamlDeploymentForClient("sales-post-enc");
AssertionUtil.decryptAssertion(responseType, (EncryptedData encryptedData) -> Collections.singletonList(deployment.getDecryptionKey()));
// check algorithm is the expected one
NodeList list = holder.getSamlDocument().getElementsByTagNameNS(JBossSAMLURIConstants.XMLENC_NSURI.get(), "EncryptedData");
assertThat("EncryptedData missing", list.getLength(), is(1));
Element encryptedData = (Element) list.item(0);
list = encryptedData.getElementsByTagNameNS(JBossSAMLURIConstants.XMLENC_NSURI.get(), "EncryptionMethod");
assertThat("EncryptionMethod missing", list.getLength(), is(2));
Element encryptionMethod = (Element) list.item(0);
assertThat("Unexpected encryption method", encryptionMethod.getAttribute("Algorithm"), is(expectedAlgorithm));
// check keyAlgorithm is the expected one
list = encryptedData.getElementsByTagNameNS(JBossSAMLURIConstants.XMLENC_NSURI.get(), "EncryptedKey");
assertThat("EncryptedKey missing", list.getLength(), is(1));
Element encryptedKey = (Element) list.item(0);
list = encryptedKey.getElementsByTagNameNS(JBossSAMLURIConstants.XMLENC_NSURI.get(), "EncryptionMethod");
assertThat("EncryptionMethod missing", list.getLength(), is(1));
encryptionMethod = (Element) list.item(0);
assertThat("Unexpected key encryption method", encryptionMethod.getAttribute("Algorithm"), is(expectedKeyAlgorithm));
// check digestMethod is the expected one or missing
if (StringUtil.isNotBlank(expectedDigestMethod)) {
list = encryptionMethod.getElementsByTagNameNS(JBossSAMLURIConstants.XMLDSIG_NSURI.get(), "DigestMethod");
assertThat("EncryptionMethod missing", list.getLength(), is(1));
Element ds = (Element) list.item(0);
assertThat("Unexpected digest method", ds.getAttribute("Algorithm"), is(expectedDigestMethod));
} else {
assertThat(encryptionMethod.getElementsByTagNameNS(JBossSAMLURIConstants.XMLDSIG_NSURI.get(), "DigestMethod").getLength(), is(0));
}
// check mgf is the expected one or missing
if (StringUtil.isNotBlank(expectedMaxskGenerationFuntion)) {
list = encryptionMethod.getElementsByTagNameNS(JBossSAMLURIConstants.XMLENC11_NSURI.get(), "MGF");
assertThat("EncryptionMethod missing", list.getLength(), is(1));
Element mgf = (Element) list.item(0);
assertThat("Unexpected mgf method", mgf.getAttribute("Algorithm"), is(expectedMaxskGenerationFuntion));
} else {
assertThat(encryptionMethod.getElementsByTagNameNS(JBossSAMLURIConstants.XMLENC11_NSURI.get(), "MGF").getLength(), is(0));
}
}
}
@Test
public void testEncryptionDefault() throws Exception {
testEncryption("", "", "", "", XMLCipher.AES_256_GCM, XMLCipher.RSA_OAEP_11, XMLCipher.SHA256, EncryptionConstants.MGF1_SHA256);
}
@Test
public void testEncryptionRsaOaep11() throws Exception {
testEncryption(XMLCipher.AES_256_GCM, XMLCipher.RSA_OAEP_11, XMLCipher.SHA512, EncryptionConstants.MGF1_SHA512);
}
@Test
public void testEncryptionRsaOaep11Default() throws Exception {
testEncryption(XMLCipher.AES_256_GCM, XMLCipher.RSA_OAEP_11, XMLCipher.SHA1, EncryptionConstants.MGF1_SHA1, XMLCipher.AES_256_GCM, XMLCipher.RSA_OAEP_11, "", "");
}
@Test
public void testEncryptionRsaOaep() throws Exception {
testEncryption(XMLCipher.AES_256_GCM, XMLCipher.RSA_OAEP, XMLCipher.SHA256, "");
}
@Test
public void testEncryptionRsaOaepLegacy() throws Exception {
testEncryption(XMLCipher.AES_128, XMLCipher.RSA_OAEP, XMLCipher.SHA1, "", XMLCipher.AES_128, XMLCipher.RSA_OAEP, "", "");
}
@Test
public void testEncryptionRsa15() throws Exception {
testEncryption(XMLCipher.AES_256_GCM, XMLCipher.RSA_v1dot5, "", "");
}
}

View File

@@ -2240,6 +2240,24 @@
"saml.authnstatement": "true",
"saml_idp_initiated_sso_url_name": "sales-post"
}
}, {
"clientId": "http://localhost:8280/sales-post-enc/",
"enabled": true,
"protocol": "saml",
"fullScopeAllowed": true,
"baseUrl": "http://localhost:8080/sales-post-enc",
"redirectUris": [
"http://localhost:8080/sales-post-enc/*"
],
"attributes": {
"saml.server.signature": "true",
"saml.signature.algorithm": "RSA_SHA512",
"saml.client.signature": "true",
"saml.encrypt": "true",
"saml.authnstatement": "true",
"saml.signing.certificate": "MIIDBjCCAe6gAwIBAgIJANPu/mvxOREdMA0GCSqGSIb3DQEBCwUAMDAxLjAsBgNVBAMTJWh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9zYWxlcy1wb3N0LWVuYy8wIBcNMjQwNjIxMTkzMTE3WhgPMjEyNDA1MjgxOTMxMTdaMDAxLjAsBgNVBAMTJWh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9zYWxlcy1wb3N0LWVuYy8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE5iKDNNW5XxHAF0ITErZcHDYZI68z7u68n7o4dsiywkfOWf7jVnw7PJVnMeDEtLWtTO6f0tRTqJ4OV5HYdJ9+mhPJtn+2UuvrepyYa2IsC1eFPH98ZEtYapsE6ObvhKBQMcu5G/tQrxkCFY2ssDa99unwBH5STLyX78UvqKiYnkPCvIhkiPIHy8ab7DQowc+EE9XhlE3b63A65rp4G9R87rwgJX5VTM3h81WcDuWLPOg7YRYLZoorWz2p38/qL9gXY5NxIRK16EHGfw2W1dPrX3GyMOJbXVyrBNZ6m5IL9Wn7lBEJ/Dl7ZFMFB5W36QkJ+3aaNLT/Tu/Gz+7f24inAgMBAAGjITAfMB0GA1UdDgQWBBSk7RegFbEBruVbt/VFl2gZhZ2IpDANBgkqhkiG9w0BAQsFAAOCAQEAGyH1sXVU3HDMhCzP2k5fsJBGA+1iKLMsyyiGcaD/22anQ1uVU7iWPZH8mSJGWqkvo/4oFb7RjB2KzO/50wP0q/P/tymGsYoznt+MEJKKxYEqAYmIns7SKRIgv3xEfF8yQy2jOuULC9FTq/Pb3gd9Om40jmeJtYccDSICjEC+A2fcGe56ScuRRLt+3WFyIZUFH7Y9FYZQ3EYQ88UZg//5F1ddAzGtdMSeTanMxLKow7rUIm/+Sx6cd+Vkwo/SYdk4hsD8xZCYx8Ln4i3NKh+SzyvbYykyWVI2fwjplqvM5Md/M+SNvPtU9tkOCUxQqVfz/bwtTiqfjdSaUJlasgGByg==",
"saml.encryption.certificate": "MIIDBjCCAe6gAwIBAgIJANPu/mvxOREdMA0GCSqGSIb3DQEBCwUAMDAxLjAsBgNVBAMTJWh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9zYWxlcy1wb3N0LWVuYy8wIBcNMjQwNjIxMTkzMTE3WhgPMjEyNDA1MjgxOTMxMTdaMDAxLjAsBgNVBAMTJWh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9zYWxlcy1wb3N0LWVuYy8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE5iKDNNW5XxHAF0ITErZcHDYZI68z7u68n7o4dsiywkfOWf7jVnw7PJVnMeDEtLWtTO6f0tRTqJ4OV5HYdJ9+mhPJtn+2UuvrepyYa2IsC1eFPH98ZEtYapsE6ObvhKBQMcu5G/tQrxkCFY2ssDa99unwBH5STLyX78UvqKiYnkPCvIhkiPIHy8ab7DQowc+EE9XhlE3b63A65rp4G9R87rwgJX5VTM3h81WcDuWLPOg7YRYLZoorWz2p38/qL9gXY5NxIRK16EHGfw2W1dPrX3GyMOJbXVyrBNZ6m5IL9Wn7lBEJ/Dl7ZFMFB5W36QkJ+3aaNLT/Tu/Gz+7f24inAgMBAAGjITAfMB0GA1UdDgQWBBSk7RegFbEBruVbt/VFl2gZhZ2IpDANBgkqhkiG9w0BAQsFAAOCAQEAGyH1sXVU3HDMhCzP2k5fsJBGA+1iKLMsyyiGcaD/22anQ1uVU7iWPZH8mSJGWqkvo/4oFb7RjB2KzO/50wP0q/P/tymGsYoznt+MEJKKxYEqAYmIns7SKRIgv3xEfF8yQy2jOuULC9FTq/Pb3gd9Om40jmeJtYccDSICjEC+A2fcGe56ScuRRLt+3WFyIZUFH7Y9FYZQ3EYQ88UZg//5F1ddAzGtdMSeTanMxLKow7rUIm/+Sx6cd+Vkwo/SYdk4hsD8xZCYx8Ln4i3NKh+SzyvbYykyWVI2fwjplqvM5Md/M+SNvPtU9tkOCUxQqVfz/bwtTiqfjdSaUJlasgGByg=="
}
}, {
"id" : "e6856a02-8f24-48d3-bb06-fae5dddae83e",
"clientId" : "realm-management",

View File

@@ -716,6 +716,25 @@
} ],
"defaultClientScopes" : [ "web-origins", "roles", "profile", "email" ],
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ]
},
{
"clientId": "http://localhost:8280/sales-post-enc/",
"enabled": true,
"protocol": "saml",
"fullScopeAllowed": true,
"baseUrl": "http://localhost:8080/sales-post-enc",
"redirectUris": [
"http://localhost:8080/sales-post-enc/*"
],
"attributes": {
"saml.server.signature": "true",
"saml.signature.algorithm": "RSA_SHA512",
"saml.client.signature": "true",
"saml.encrypt": "true",
"saml.authnstatement": "true",
"saml.signing.certificate": "MIIDBjCCAe6gAwIBAgIJANPu/mvxOREdMA0GCSqGSIb3DQEBCwUAMDAxLjAsBgNVBAMTJWh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9zYWxlcy1wb3N0LWVuYy8wIBcNMjQwNjIxMTkzMTE3WhgPMjEyNDA1MjgxOTMxMTdaMDAxLjAsBgNVBAMTJWh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9zYWxlcy1wb3N0LWVuYy8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE5iKDNNW5XxHAF0ITErZcHDYZI68z7u68n7o4dsiywkfOWf7jVnw7PJVnMeDEtLWtTO6f0tRTqJ4OV5HYdJ9+mhPJtn+2UuvrepyYa2IsC1eFPH98ZEtYapsE6ObvhKBQMcu5G/tQrxkCFY2ssDa99unwBH5STLyX78UvqKiYnkPCvIhkiPIHy8ab7DQowc+EE9XhlE3b63A65rp4G9R87rwgJX5VTM3h81WcDuWLPOg7YRYLZoorWz2p38/qL9gXY5NxIRK16EHGfw2W1dPrX3GyMOJbXVyrBNZ6m5IL9Wn7lBEJ/Dl7ZFMFB5W36QkJ+3aaNLT/Tu/Gz+7f24inAgMBAAGjITAfMB0GA1UdDgQWBBSk7RegFbEBruVbt/VFl2gZhZ2IpDANBgkqhkiG9w0BAQsFAAOCAQEAGyH1sXVU3HDMhCzP2k5fsJBGA+1iKLMsyyiGcaD/22anQ1uVU7iWPZH8mSJGWqkvo/4oFb7RjB2KzO/50wP0q/P/tymGsYoznt+MEJKKxYEqAYmIns7SKRIgv3xEfF8yQy2jOuULC9FTq/Pb3gd9Om40jmeJtYccDSICjEC+A2fcGe56ScuRRLt+3WFyIZUFH7Y9FYZQ3EYQ88UZg//5F1ddAzGtdMSeTanMxLKow7rUIm/+Sx6cd+Vkwo/SYdk4hsD8xZCYx8Ln4i3NKh+SzyvbYykyWVI2fwjplqvM5Md/M+SNvPtU9tkOCUxQqVfz/bwtTiqfjdSaUJlasgGByg==",
"saml.encryption.certificate": "MIIDBjCCAe6gAwIBAgIJANPu/mvxOREdMA0GCSqGSIb3DQEBCwUAMDAxLjAsBgNVBAMTJWh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9zYWxlcy1wb3N0LWVuYy8wIBcNMjQwNjIxMTkzMTE3WhgPMjEyNDA1MjgxOTMxMTdaMDAxLjAsBgNVBAMTJWh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9zYWxlcy1wb3N0LWVuYy8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE5iKDNNW5XxHAF0ITErZcHDYZI68z7u68n7o4dsiywkfOWf7jVnw7PJVnMeDEtLWtTO6f0tRTqJ4OV5HYdJ9+mhPJtn+2UuvrepyYa2IsC1eFPH98ZEtYapsE6ObvhKBQMcu5G/tQrxkCFY2ssDa99unwBH5STLyX78UvqKiYnkPCvIhkiPIHy8ab7DQowc+EE9XhlE3b63A65rp4G9R87rwgJX5VTM3h81WcDuWLPOg7YRYLZoorWz2p38/qL9gXY5NxIRK16EHGfw2W1dPrX3GyMOJbXVyrBNZ6m5IL9Wn7lBEJ/Dl7ZFMFB5W36QkJ+3aaNLT/Tu/Gz+7f24inAgMBAAGjITAfMB0GA1UdDgQWBBSk7RegFbEBruVbt/VFl2gZhZ2IpDANBgkqhkiG9w0BAQsFAAOCAQEAGyH1sXVU3HDMhCzP2k5fsJBGA+1iKLMsyyiGcaD/22anQ1uVU7iWPZH8mSJGWqkvo/4oFb7RjB2KzO/50wP0q/P/tymGsYoznt+MEJKKxYEqAYmIns7SKRIgv3xEfF8yQy2jOuULC9FTq/Pb3gd9Om40jmeJtYccDSICjEC+A2fcGe56ScuRRLt+3WFyIZUFH7Y9FYZQ3EYQ88UZg//5F1ddAzGtdMSeTanMxLKow7rUIm/+Sx6cd+Vkwo/SYdk4hsD8xZCYx8Ln4i3NKh+SzyvbYykyWVI2fwjplqvM5Md/M+SNvPtU9tkOCUxQqVfz/bwtTiqfjdSaUJlasgGByg=="
}
} ],
"clientScopes" : [ {
"id" : "adef1610-70ec-4282-88ef-bcb26b1f5edf",

View File

@@ -2951,6 +2951,25 @@
} ],
"defaultClientScopes" : [ "web-origins", "roles", "profile", "email" ],
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ]
},
{
"clientId": "http://localhost:8280/sales-post-enc/",
"enabled": true,
"protocol": "saml",
"fullScopeAllowed": true,
"baseUrl": "http://localhost:8080/sales-post-enc",
"redirectUris": [
"http://localhost:8080/sales-post-enc/*"
],
"attributes": {
"saml.server.signature": "true",
"saml.signature.algorithm": "RSA_SHA512",
"saml.client.signature": "true",
"saml.encrypt": "true",
"saml.authnstatement": "true",
"saml.signing.certificate": "MIIDBjCCAe6gAwIBAgIJANPu/mvxOREdMA0GCSqGSIb3DQEBCwUAMDAxLjAsBgNVBAMTJWh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9zYWxlcy1wb3N0LWVuYy8wIBcNMjQwNjIxMTkzMTE3WhgPMjEyNDA1MjgxOTMxMTdaMDAxLjAsBgNVBAMTJWh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9zYWxlcy1wb3N0LWVuYy8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE5iKDNNW5XxHAF0ITErZcHDYZI68z7u68n7o4dsiywkfOWf7jVnw7PJVnMeDEtLWtTO6f0tRTqJ4OV5HYdJ9+mhPJtn+2UuvrepyYa2IsC1eFPH98ZEtYapsE6ObvhKBQMcu5G/tQrxkCFY2ssDa99unwBH5STLyX78UvqKiYnkPCvIhkiPIHy8ab7DQowc+EE9XhlE3b63A65rp4G9R87rwgJX5VTM3h81WcDuWLPOg7YRYLZoorWz2p38/qL9gXY5NxIRK16EHGfw2W1dPrX3GyMOJbXVyrBNZ6m5IL9Wn7lBEJ/Dl7ZFMFB5W36QkJ+3aaNLT/Tu/Gz+7f24inAgMBAAGjITAfMB0GA1UdDgQWBBSk7RegFbEBruVbt/VFl2gZhZ2IpDANBgkqhkiG9w0BAQsFAAOCAQEAGyH1sXVU3HDMhCzP2k5fsJBGA+1iKLMsyyiGcaD/22anQ1uVU7iWPZH8mSJGWqkvo/4oFb7RjB2KzO/50wP0q/P/tymGsYoznt+MEJKKxYEqAYmIns7SKRIgv3xEfF8yQy2jOuULC9FTq/Pb3gd9Om40jmeJtYccDSICjEC+A2fcGe56ScuRRLt+3WFyIZUFH7Y9FYZQ3EYQ88UZg//5F1ddAzGtdMSeTanMxLKow7rUIm/+Sx6cd+Vkwo/SYdk4hsD8xZCYx8Ln4i3NKh+SzyvbYykyWVI2fwjplqvM5Md/M+SNvPtU9tkOCUxQqVfz/bwtTiqfjdSaUJlasgGByg==",
"saml.encryption.certificate": "MIIDBjCCAe6gAwIBAgIJANPu/mvxOREdMA0GCSqGSIb3DQEBCwUAMDAxLjAsBgNVBAMTJWh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9zYWxlcy1wb3N0LWVuYy8wIBcNMjQwNjIxMTkzMTE3WhgPMjEyNDA1MjgxOTMxMTdaMDAxLjAsBgNVBAMTJWh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9zYWxlcy1wb3N0LWVuYy8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE5iKDNNW5XxHAF0ITErZcHDYZI68z7u68n7o4dsiywkfOWf7jVnw7PJVnMeDEtLWtTO6f0tRTqJ4OV5HYdJ9+mhPJtn+2UuvrepyYa2IsC1eFPH98ZEtYapsE6ObvhKBQMcu5G/tQrxkCFY2ssDa99unwBH5STLyX78UvqKiYnkPCvIhkiPIHy8ab7DQowc+EE9XhlE3b63A65rp4G9R87rwgJX5VTM3h81WcDuWLPOg7YRYLZoorWz2p38/qL9gXY5NxIRK16EHGfw2W1dPrX3GyMOJbXVyrBNZ6m5IL9Wn7lBEJ/Dl7ZFMFB5W36QkJ+3aaNLT/Tu/Gz+7f24inAgMBAAGjITAfMB0GA1UdDgQWBBSk7RegFbEBruVbt/VFl2gZhZ2IpDANBgkqhkiG9w0BAQsFAAOCAQEAGyH1sXVU3HDMhCzP2k5fsJBGA+1iKLMsyyiGcaD/22anQ1uVU7iWPZH8mSJGWqkvo/4oFb7RjB2KzO/50wP0q/P/tymGsYoznt+MEJKKxYEqAYmIns7SKRIgv3xEfF8yQy2jOuULC9FTq/Pb3gd9Om40jmeJtYccDSICjEC+A2fcGe56ScuRRLt+3WFyIZUFH7Y9FYZQ3EYQ88UZg//5F1ddAzGtdMSeTanMxLKow7rUIm/+Sx6cd+Vkwo/SYdk4hsD8xZCYx8Ln4i3NKh+SzyvbYykyWVI2fwjplqvM5Md/M+SNvPtU9tkOCUxQqVfz/bwtTiqfjdSaUJlasgGByg=="
}
} ],
"clientScopes" : [ {
"id" : "adef1610-70ec-4282-88ef-bcb26b1f5edf",
@@ -5895,4 +5914,4 @@
"clientPolicies" : {
"policies" : [ ]
}
} ]
} ]