pl.edu.icm.unity.saml.sp.SAMLSPProperties Maven / Gradle / Ivy
/*
* Copyright (c) 2014 ICM Uniwersytet Warszawski All rights reserved.
* See LICENCE.txt file for licensing information.
*/
package pl.edu.icm.unity.saml.sp;
import eu.emi.security.authn.x509.X509Credential;
import eu.unicore.util.configuration.ConfigurationException;
import eu.unicore.util.configuration.DocumentationReferenceMeta;
import eu.unicore.util.configuration.DocumentationReferencePrefix;
import eu.unicore.util.configuration.PropertyMD;
import eu.unicore.util.configuration.PropertyMD.DocumentationCategory;
import io.imunity.vaadin.auth.CommonWebAuthnProperties;
import org.apache.logging.log4j.Logger;
import pl.edu.icm.unity.base.exceptions.EngineException;
import pl.edu.icm.unity.base.utils.Log;
import pl.edu.icm.unity.engine.api.PKIManagement;
import pl.edu.icm.unity.saml.SamlProperties;
import pl.edu.icm.unity.saml.ecp.SAMLECPProperties;
import pl.edu.icm.unity.saml.sp.config.AdditionalCredential;
import xmlbeans.org.oasis.saml2.assertion.NameIDType;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.*;
/**
* Configuration of a SAML requester (or SAML SP).
* @author K. Benedyczak
*/
public class SAMLSPProperties extends SamlProperties
{
private static final Logger log = Log.getLogger(Log.U_SERVER_CFG, SAMLSPProperties.class);
public enum MetadataSignatureValidation {require, ignore};
@DocumentationReferencePrefix
public static final String P = "unity.saml.requester.";
@DocumentationReferenceMeta
public final static Map META = new HashMap<>();
public static final String REQUESTER_ID = "requesterEntityId";
public static final String CREDENTIAL = "requesterCredential";
public static final String ADDITIONAL_CREDENTIAL = "additionalCredential";
public static final String INCLUDE_ADDITIONAL_CREDENTIAL_IN_METADATA = "includeAddtionalCredentialInMetadata";
public static final String ACCEPTED_NAME_FORMATS = "acceptedNameFormats.";
public static final String METADATA_PATH = "metadataPath";
public static final String SLO_PATH = "sloPath";
public static final String SLO_REALM = "sloRealm";
public static final String DEF_SIGN_REQUEST = "defaultSignRequest";
public static final String DEF_REQUESTED_NAME_FORMAT = "defaultRequestedNameFormat";
public static final String REQUIRE_SIGNED_ASSERTION = "requireSignedAssertion";
public static final String IDPMETA_PREFIX = "metadataSource.";
public static final String IDPMETA_TRANSLATION_PROFILE = "perMetadataTranslationProfile";
public static final String IDPMETA_EMBEDDED_TRANSLATION_PROFILE = "perMetadataEmbeddedTranslationProfile";
public static final String IDPMETA_REGISTRATION_FORM = "perMetadataRegistrationForm";
public static final String IDPMETA_EXCLUDED_IDPS = "excludedIdps.";
public static final String IDPMETA_FEDERATION_IDP_FILTER = "federationIdpFilter";
public static final String IDP_FEDERATION_ID = "samlFederationId";
public static final String IDP_FEDERATION_NAME = "samlFederationName";
public static final String IDP_PREFIX = "remoteIdp.";
public static final String IDP_NAME = "name";
public static final String IDP_LOGO = "logoURI";
public static final String IDP_ID = "samlId";
public static final String IDP_ADDRESS = "address";
public static final String IDP_BINDING = "binding";
public static final String IDP_CERTIFICATE = "certificate";
public static final String IDP_CERTIFICATES = "certificates.";
public static final String IDP_SIGN_REQUEST = "signRequest";
public static final String IDP_REQUESTED_NAME_FORMAT = "requestedNameFormat";
public static final String IDP_GROUP_MEMBERSHIP_ATTRIBUTE = "groupMembershipAttribute";
public static final String DEFAULT_TRANSLATION_PROFILE = "sys:saml";
public static final Binding DEFAULT_IDP_BINDING = Binding.HTTP_REDIRECT;
static
{
DocumentationCategory common = new DocumentationCategory(
"Common settings", "01");
DocumentationCategory idp = new DocumentationCategory(
"Manual settings of trusted IdPs", "03");
META.put(IDP_PREFIX, new PropertyMD().setStructuredList(false).setCategory(idp).setDescription(
"With this prefix configuration of trusted and enabled remote SAML IdPs is stored. " +
"There must be at least one IdP defined. If there are multiple ones defined, then the user can choose which one to use."));
META.put(IDP_ADDRESS, new PropertyMD().setStructuredListEntry(IDP_PREFIX).setCategory(idp).setDescription(
"Address of the IdP endpoint."));
META.put(IDP_BINDING, new PropertyMD(DEFAULT_IDP_BINDING).setStructuredListEntry(IDP_PREFIX).setCategory(idp).setDescription(
"SAML binding to be used to send a request to the IdP. If you use 'SOAP' here then the IdP will be available only for ECP logins, not via the web browser login."));
META.put(REDIRECT_LOGOUT_URL, new PropertyMD().setStructuredListEntry(IDP_PREFIX).setCategory(idp).setDescription(
"Address of the IdP Single Logout Endpoint supporting HTTP Redirect binding."));
META.put(REDIRECT_LOGOUT_RET_URL, new PropertyMD().setStructuredListEntry(IDP_PREFIX).setCategory(idp).setDescription(
"Address of the IdP Single Logout response endpoint supporting HTTP Redirect binding. "
+ "If undefined the base redirect endpoint address is used."));
META.put(POST_LOGOUT_URL, new PropertyMD().setStructuredListEntry(IDP_PREFIX).setCategory(idp).setDescription(
"Address of the IdP Single Logout Endpoint supporting HTTP POST binding."));
META.put(POST_LOGOUT_RET_URL, new PropertyMD().setStructuredListEntry(IDP_PREFIX).setCategory(idp).setDescription(
"Address of the IdP Single Logout response endpoint supporting HTTP POST binding. "
+ "If undefined the base redirect endpoint address is used."));
META.put(SOAP_LOGOUT_URL, new PropertyMD().setStructuredListEntry(IDP_PREFIX).setCategory(idp).setDescription(
"Address of the IdP Single Logout Endpoint supporting SOAP binding."));
META.put(IDP_NAME, new PropertyMD().setStructuredListEntry(IDP_PREFIX).setCategory(idp).setCanHaveSubkeys().setDescription(
"Displayed name of the IdP. If not defined then the name is created " +
"from the IdP address (what is rather not user friendly). The property can have subkeys being "
+ "locale names; then the localized value is used if it is matching the selected locale of the UI."));
META.put(IDP_LOGO, new PropertyMD().setStructuredListEntry(IDP_PREFIX).setCategory(idp).setCanHaveSubkeys().setDescription(
"Displayed logo of the IdP. If not defined then only the name is used. "
+ "The value can be a file:, http(s): or data: URI. The last option allows for embedding the logo in the configuration. "
+ "The property can have subkeys being "
+ "locale names; then the localized value is used if it is matching the selected locale of the UI."));
META.put(IDP_FEDERATION_ID, new PropertyMD().setStructuredListEntry(IDP_PREFIX).setCategory(idp).setHidden().setDescription(
"SAML federation identifier of the IdP."));
META.put(IDP_FEDERATION_NAME, new PropertyMD().setStructuredListEntry(IDP_PREFIX).setCategory(idp).setHidden().setDescription(
"SAML federation name of the IdP."));
META.put(IDP_ID, new PropertyMD().setStructuredListEntry(IDP_PREFIX).setMandatory().setCategory(idp).setDescription(
"SAML entity identifier of the IdP."));
META.put(IDP_CERTIFICATE, new PropertyMD().setStructuredListEntry(IDP_PREFIX).setCategory(idp).setDescription(
"Certificate name (as used in centralized PKI store) of the IdP. This certificate is used to verify signature of SAML " +
"response and included assertions. Therefore it is of highest importance for the whole system security."));
META.put(IDP_CERTIFICATES, new PropertyMD().setStructuredListEntry(IDP_PREFIX).setCategory(idp).setList(false).setDescription(
"Using this property additional trusted certificates of an IdP can be added (when IdP uses more then one). See "
+ IDP_CERTIFICATE + " for details. Those properties can be used together or alternatively."));
META.put(IDP_SIGN_REQUEST, new PropertyMD("false").setCategory(idp).setStructuredListEntry(IDP_PREFIX).setDescription(
"Controls whether the requests for this IdP should be signed."));
META.put(IDP_REQUESTED_NAME_FORMAT, new PropertyMD().setCategory(idp).setStructuredListEntry(IDP_PREFIX).setDescription(
"If defined then specifies what SAML name format should be requested from the IdP." +
" If undefined then IdP is free to choose, however see the " + ACCEPTED_NAME_FORMATS +
" property. Value is arbitrary string, meaningful for the IdP. SAML specifies several standard formats:" +
" +urn:oasis:names:tc:SAML:2.0:nameid-format:persistent+," +
" +urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress+," +
" +urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName+ and " +
" +urn:oasis:names:tc:SAML:2.0:nameid-format:transient+ are the most popular."));
META.put(IDP_GROUP_MEMBERSHIP_ATTRIBUTE, new PropertyMD().setCategory(idp).setStructuredListEntry(IDP_PREFIX).setDescription(
"Defines a SAML attribute name which will be treated as an attribute carrying group" +
" membership information."));
META.put(CommonWebAuthnProperties.TRANSLATION_PROFILE, new PropertyMD(DEFAULT_TRANSLATION_PROFILE).setCategory(idp).setStructuredListEntry(IDP_PREFIX).
setDescription("Name of a translation" +
" profile, which will be used to map remotely obtained attributes and identity" +
" to the local counterparts. The profile should at least map the remote identity."));
META.put(CommonWebAuthnProperties.EMBEDDED_TRANSLATION_PROFILE, new PropertyMD().setCategory(idp).setHidden()
.setStructuredListEntry(IDP_PREFIX)
.setDescription("Translation"
+ " profile, which will be used to map remotely obtained attributes and identity"
+ " to the local counterparts. The profile should at least map the remote identity."));
META.put(CommonWebAuthnProperties.REGISTRATION_FORM, new PropertyMD().setCategory(idp).setStructuredListEntry(IDP_PREFIX).setDescription(
"Name of a registration form to be shown for a remotely authenticated principal who " +
"has no local account. If unset such users will be denied."));
META.put(CommonWebAuthnProperties.ENABLE_ASSOCIATION, new PropertyMD().setBoolean().setCategory(idp).
setStructuredListEntry(IDP_PREFIX).setDescription(
"If true then unknown remote user gets an option to associate the remote identity "
+ "with an another local (already existing) account. Overrides the global setting."));
META.put(REQUESTER_ID, new PropertyMD().setMandatory().setCategory(common).setDescription(
"SAML entity ID (must be a URI) of the local SAML requester (or service provider)."));
META.put(CREDENTIAL, new PropertyMD().setCategory(common).setDescription(
"Local credential, used to sign requests and to decrypt encrypted assertions. "
+ "If neither signing nor decryption is used it can be skipped."));
META.put(ADDITIONAL_CREDENTIAL, new PropertyMD().setCategory(common).setDescription(
"Additional local credential, used to decrypt encrypted assertions."));
META.put(INCLUDE_ADDITIONAL_CREDENTIAL_IN_METADATA, new PropertyMD("false").setCategory(common).setDescription(
"Include additional credential in metadata"));
META.put(SLO_PATH, new PropertyMD().setCategory(common).setDescription(
"Last element of the URL, under which the SAML Single Logout functionality should "
+ "be published for this SAML authenticator. Any suffix can be used, however it "
+ "must be unique for all SAML authenticators in the system. If undefined the SLO functionality won't be enabled."));
META.put(SLO_REALM, new PropertyMD().setCategory(common).setDescription(
"Name of the authentication realm of the endpoints using this authenticator. "
+ "This is needed to enable Single Logout functionality (if undefined the SLO "
+ "functionality will be disabled). If this authenticator is used by endpoints placed in different realms and "
+ "you still want to have SLO functionality you have to define one authenticator per realm."));
META.put(METADATA_PATH, new PropertyMD().setCategory(SamlProperties.samlMetaCat).setDescription(
"Last element of the URL, under which the SAML metadata should be published for this SAML authenticator." +
"Used only if metadata publication is enabled. See the SAML Metadata section for more details."));
META.put(ACCEPTED_NAME_FORMATS, new PropertyMD().setList(false).setCategory(common).setDescription(
"If defined then specifies what SAML name formatd are accepted from IdP. " +
"Useful when the property " + IDP_REQUESTED_NAME_FORMAT + " is undefined for at least one IdP. "));
META.put(REQUIRE_SIGNED_ASSERTION, new PropertyMD("false").setCategory(common).setDescription(
"SAML authN responses may be signed as a whole and/or may have signed individual assertions"
+ " which are contained in the response. In general SAML SSO protocol requires "
+ "assertions to be signed, but in the wild this is not always the case. If this option"
+ "is set to false, then response will be accepted also when it is signed, "
+ "but its assertions are not."));
META.put(DEF_SIGN_REQUEST, new PropertyMD("false").setCategory(common).setDescription(
"Default setting of request signing. Used for those IdPs, for which the setting is not set explicitly."));
META.put(DEF_REQUESTED_NAME_FORMAT, new PropertyMD().setCategory(common).setDescription(
"Default setting of requested identity format. Used for those IdPs, for which the setting is not set explicitly."));
META.put(CommonWebAuthnProperties.DEF_ENABLE_ASSOCIATION, new PropertyMD("true").setCategory(common).setDescription(
"Default setting allowing to globally control whether account association feature is enabled. "
+ "Used for those IdPs, for which the setting is not set explicitly."));
META.put(SAMLECPProperties.JWT_P, new PropertyMD().setCanHaveSubkeys().setHidden());
META.put(IDPMETA_TRANSLATION_PROFILE, new PropertyMD().setCategory(remoteMeta).
setStructuredListEntry(IDPMETA_PREFIX).setDescription(
"Deafult translation profile for all the IdPs from the metadata. "
+ "Can be overwritten by individual IdP configuration entries."));
META.put(IDPMETA_EMBEDDED_TRANSLATION_PROFILE, new PropertyMD().setCategory(remoteMeta).setHidden().
setStructuredListEntry(IDPMETA_PREFIX).setDescription(
"Deafult translation profile for all the IdPs from the metadata. "
+ "Can be overwritten by individual IdP configuration entries."));
META.put(IDPMETA_REGISTRATION_FORM, new PropertyMD().setCategory(remoteMeta).
setStructuredListEntry(IDPMETA_PREFIX).setDescription(
"Deafult registration form for all the IdPs from the metadata. Can be overwritten by "
+ "individual IdP configuraiton entries."));
META.put(IDPMETA_EXCLUDED_IDPS, new PropertyMD().setStructuredListEntry(IDPMETA_PREFIX).setCategory(idp).setList(false).setDescription(
"List of excluded SAML IdP entity identifiers"));
META.put(IDPMETA_FEDERATION_IDP_FILTER, new PropertyMD().setCategory(remoteMeta)
.setStructuredListEntry(IDPMETA_PREFIX)
.setDescription("If a filter is configured then all federation IdPs will be tested against it."
+ " Filter expression can use IdP attributes, as obtained from federation metadata, to decide whether to include it or not."
+ " If expression returns true the IdP will be trusted, and otherwise will be excluded."));
META.put(IDENTITY_MAPPING_PFX, new PropertyMD().setStructuredList(false).setCategory(common).
setDescription("Prefix used to store mappings of SAML identity types to Unity identity types. "
+ "Those mappings are used to reverse the mapping process of remote identity "
+ "mapping into Unity representation (as configured with an input translation profile). "
+ "This is used solely to provide a single logout functionality, "
+ "where remote peer may request to logout an identity previously "
+ "authenticated. Unity needs to be able to find this person's session to terminate it."));
META.put(IDENTITY_LOCAL, new PropertyMD().setStructuredListEntry(IDENTITY_MAPPING_PFX).setMandatory().setCategory(common).
setDescription("Unity identity to which the SAML identity is mapped. If it is set to an empty value, then the mapping is disabled, "
+ "what is useful for turning off the default mappings."));
META.put(IDENTITY_SAML, new PropertyMD().setStructuredListEntry(IDENTITY_MAPPING_PFX).setMandatory().setCategory(common).
setDescription("SAML identity to be mapped"));
META.putAll(SamlProperties.getDefaults(IDPMETA_PREFIX, "Under this prefix you can configure "
+ "the remote trusted SAML IdPs however not providing all their details but only "
+ "their metadata."));
}
private PKIManagement pkiManagement;
private Properties sourceProperties;
public SAMLSPProperties(Properties properties, PKIManagement pkiMan) throws ConfigurationException
{
this(properties, META, pkiMan);
}
/**
* For cloning only.
*/
protected SAMLSPProperties(SAMLSPProperties cloned) throws ConfigurationException
{
super(cloned);
this.pkiManagement = cloned.pkiManagement;
this.sourceProperties = new Properties(cloned.sourceProperties);
}
protected SAMLSPProperties(Properties properties, Map meta,
PKIManagement pkiMan) throws ConfigurationException
{
super(P, properties, meta, log);
addCachedPrefixes("unity\\.saml\\.requester\\.remoteIdp\\.[^.]+\\.certificates\\.",
"unity\\.saml\\.requester\\.remoteIdp\\.[^.]+\\.name\\.");
sourceProperties = new Properties();
sourceProperties.putAll(properties);
this.pkiManagement = pkiMan;
Set idpKeys = getStructuredListKeys(IDP_PREFIX);
boolean sign = false;
for (String idpKey: idpKeys)
{
boolean s = isSignRequest(idpKey);
sign |= s;
Binding b = getEnumValue(idpKey+IDP_BINDING, Binding.class);
if (s && (b == Binding.HTTP_REDIRECT || b == Binding.SOAP))
{
String name = getName(idpKey);
throw new ConfigurationException("IdP " + name + " is configured to use " +
"HTTP Redirect binding or SOAP binding for ECP and at "
+ "the same time Unity is configured to sign requests for this IdP. "
+ "This is unsupported.");
}
}
if (sign)
{
String credential = getValue(CREDENTIAL);
if (credential == null)
throw new ConfigurationException("Credential must be defined when " +
"request signing is enabled for at least one IdP.");
try
{
if (!pkiMan.getCredentialNames().contains(credential))
throw new ConfigurationException("Credential name is invalid - there is no such " +
"credential available '" + credential + "'.");
} catch (EngineException e)
{
throw new ConfigurationException("Can't esablish a list of known credentials", e);
}
}
Set metaKeys = getStructuredListKeys(IDPMETA_PREFIX);
Set certs;
try
{
certs = pkiManagement.getAllCertificateNames();
} catch (EngineException e)
{
throw new ConfigurationException("Can't retrieve available certificates", e);
}
for (String metaKey: metaKeys)
{
MetadataSignatureValidation validation = getEnumValue(metaKey + METADATA_SIGNATURE,
MetadataSignatureValidation.class);
if (validation == MetadataSignatureValidation.require)
{
String certName = getValue(metaKey + METADATA_ISSUER_CERT);
if (certName == null)
throw new ConfigurationException("For the " + metaKey +
" entry the certificate for metadata signature verification is not set");
if (!certs.contains(certName))
throw new ConfigurationException("For the " + metaKey +
" entry the certificate for metadata signature "
+ "verification is incorrect: " + certName);
}
}
verifyTrustdedCertificatesExistence();
if (getBooleanValue(PUBLISH_METADATA) && !isSet(METADATA_PATH))
throw new ConfigurationException("Metadata path " + getKeyDescription(METADATA_PATH) +
" must be set if CheckingMode modemetadata publication is enabled.");
}
@Override
public synchronized void setProperties(Properties properties) throws ConfigurationException
{
long start = System.currentTimeMillis();
super.setProperties(properties);
log.info("Updated trusted IdPs configuration with " + getStructuredListKeys(IDP_PREFIX).size()
+ " explicit trusted providers, took " + (System.currentTimeMillis() - start) + "ms");
}
public X509Credential getRequesterCredential()
{
return getCredential(SAMLSPProperties.CREDENTIAL);
}
public Optional getAdditionalRequesterCredential()
{
String credName = getValue(SAMLSPProperties.ADDITIONAL_CREDENTIAL);
if (credName == null || credName.isEmpty())
return Optional.empty();
return Optional.of(new AdditionalCredential(credName, getCredential(SAMLSPProperties.ADDITIONAL_CREDENTIAL)));
}
private X509Credential getCredential(String credentialKey)
{
String credential = getValue(credentialKey);
try
{
return pkiManagement.getCredential(credential);
} catch (EngineException e)
{
return null;
}
}
private void verifyTrustdedCertificatesExistence() throws ConfigurationException
{
Set idpKeys = getStructuredListKeys(IDP_PREFIX);
for (String idpKey: idpKeys)
{
Set idpCertNames = getCertificateNames(idpKey);
for (String idpCertName: idpCertNames)
{
try
{
pkiManagement.getCertificate(idpCertName);
} catch (EngineException e)
{
throw new ConfigurationException("Remote SAML IdP certificate can not be loaded "
+ idpCertName, e);
}
}
}
}
public List getPublicKeysOfIdp(String idpKey)
{
Set idpCertNames = getCertificateNames(idpKey);
List keys = new ArrayList<>();
for (String idpCertName: idpCertNames)
{
try
{
X509Certificate idpCert = pkiManagement.getCertificate(idpCertName).value;
keys.add(idpCert.getPublicKey());
} catch (EngineException e)
{
throw new ConfigurationException("Remote SAML IdP certificate can not be loaded "
+ idpCertName, e);
}
}
return keys;
}
public Set getCertificateNames(String idpKey)
{
return getCertificateNames(idpKey, IDP_CERTIFICATE, IDP_CERTIFICATES);
}
public boolean isSignRequest(String idpKey)
{
return isSet(idpKey + IDP_SIGN_REQUEST) ?
getBooleanValue(idpKey + IDP_SIGN_REQUEST) :
getBooleanValue(DEF_SIGN_REQUEST);
}
public String getRequestedNameFormat(String idpKey)
{
return isSet(idpKey + IDP_REQUESTED_NAME_FORMAT) ?
getValue(idpKey + IDP_REQUESTED_NAME_FORMAT) :
getValue(DEF_REQUESTED_NAME_FORMAT);
}
/**
* As trusted IdP entries can be partially created from default values and/or generated from remote metadata
* it may happen that some of the entries are in the end incomplete. This method verifies this.
*
* @param key
* @return
*/
public boolean isIdPDefinitionComplete(String key)
{
String entityId;
if (!isSet(key + IDP_ID))
{
log.warn("No entityId for " + key + " ignoring IdP");
return false;
} else
entityId = getValue(key + IDP_ID);
if (!isSet(key + IDP_ADDRESS))
{
log.warn("No address for " + entityId + " ignoring IdP");
return false;
}
if (getCertificateNames(key).size() == 0)
{
log.warn("No certificate for " + entityId + " ignoring IdP");
return false;
}
String translatioProfile = getValue(key + CommonWebAuthnProperties.TRANSLATION_PROFILE);
String embeddedTranslatioProfile = getValue(
key + CommonWebAuthnProperties.EMBEDDED_TRANSLATION_PROFILE);
if ((translatioProfile == null || translatioProfile.isEmpty())
&& (embeddedTranslatioProfile == null || embeddedTranslatioProfile.isEmpty()))
{
log.warn("No translation profile for " + entityId + " ignoring IdP");
return false;
}
return true;
}
public String getIdPConfigKey(NameIDType requester)
{
return getPrefixOfIdP(requester.getStringValue());
}
/**
* @return original properties, i.e. those which were used to configure the authenticator.
* The {@link #getProperties()} returns runtime properties which can include additional entries
* added from remote metadata. Always a copy is returned.
*/
public Properties getSourceProperties()
{
Properties configProps = new Properties();
configProps.putAll(sourceProperties);
return configProps;
}
public String getLocalizedName(String idpKey, Locale locale)
{
String ret = getLocalizedValue(idpKey + IDP_NAME, locale);
return ret != null ? ret : getName(idpKey);
}
/**
* @param idpKey
* @return idp name if set, otherwise its id which is mandatory.
*/
public String getName(String idpKey)
{
String key = idpKey + IDP_NAME;
return isSet(key) ? getValue(key) : getValue(idpKey + IDP_ID);
}
public String getPrefixOfIdP(String entity)
{
Set keys = getStructuredListKeys(IDP_PREFIX);
for (String key: keys)
{
if (entity.equals(getValue(key+IDP_ID)))
return key;
}
return null;
}
@Override
public SAMLSPProperties clone()
{
return new SAMLSPProperties(this);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy