KEYCLOAK-18315 SAML Client - Add parameter to request specific AttributeConsumingServiceIndex

This commit is contained in:
Luca Leonardo Scorcia 2021-05-30 06:43:58 -04:00 committed by Hynek Mlnařík
parent 9dc7300178
commit ae98d8ea28
6 changed files with 142 additions and 2 deletions

View file

@ -75,6 +75,11 @@ public class SAML2AuthnRequestBuilder implements SamlProtocolExtensionsAwareBuil
return this;
}
public SAML2AuthnRequestBuilder attributeConsumingServiceIndex(Integer attributeConsumingServiceIndex) {
this.authnRequestType.setAttributeConsumingServiceIndex(attributeConsumingServiceIndex);
return this;
}
public SAML2AuthnRequestBuilder forceAuthn(boolean forceAuthn) {
this.authnRequestType.setForceAuthn(forceAuthn);
return this;
@ -85,8 +90,8 @@ public class SAML2AuthnRequestBuilder implements SamlProtocolExtensionsAwareBuil
return this;
}
public SAML2AuthnRequestBuilder nameIdPolicy(SAML2NameIDPolicyBuilder nameIDPolicy) {
this.authnRequestType.setNameIDPolicy(nameIDPolicy.build());
public SAML2AuthnRequestBuilder nameIdPolicy(SAML2NameIDPolicyBuilder nameIDPolicyBuilder) {
this.authnRequestType.setNameIDPolicy(nameIDPolicyBuilder.build());
return this;
}

View file

@ -130,6 +130,8 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
for (String authnContextDeclRef : getAuthnContextDeclRefUris())
requestedAuthnContext.addAuthnContextDeclRef(authnContextDeclRef);
Integer attributeConsumingServiceIndex = getConfig().getAttributeConsumingServiceIndex();
String loginHint = getConfig().isLoginHint() ? request.getAuthenticationSession().getClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM) : null;
Boolean allowCreate = null;
if (getConfig().getConfig().get(SAMLIdentityProviderConfig.ALLOW_CREATE) == null || getConfig().isAllowCreate())
@ -143,6 +145,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
.nameIdPolicy(SAML2NameIDPolicyBuilder
.format(nameIDPolicyFormat)
.setAllowCreate(allowCreate))
.attributeConsumingServiceIndex(attributeConsumingServiceIndex)
.requestedAuthnContext(requestedAuthnContext)
.subject(loginHint);

View file

@ -60,6 +60,7 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
public static final String AUTHN_CONTEXT_DECL_REFS = "authnContextDeclRefs";
public static final String SIGN_SP_METADATA = "signSpMetadata";
public static final String ALLOW_CREATE = "allowCreate";
public static final String ATTRIBUTE_CONSUMING_SERVICE_INDEX = "attributeConsumingServiceIndex";
public SAMLIdentityProviderConfig() {
}
@ -345,6 +346,30 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
getConfig().put(ALLOW_CREATE, String.valueOf(allowCreate));
}
public Integer getAttributeConsumingServiceIndex() {
Integer result = null;
String strAttributeConsumingServiceIndex = getConfig().get(ATTRIBUTE_CONSUMING_SERVICE_INDEX);
if (strAttributeConsumingServiceIndex != null && !strAttributeConsumingServiceIndex.isEmpty()) {
try {
result = Integer.parseInt(strAttributeConsumingServiceIndex);
if (result < 0) {
result = null;
}
} catch (NumberFormatException e) {
// ignore it and use null
}
}
return result;
}
public void setAttributeConsumingServiceIndex(Integer attributeConsumingServiceIndex) {
if (attributeConsumingServiceIndex == null || attributeConsumingServiceIndex < 0) {
getConfig().remove(ATTRIBUTE_CONSUMING_SERVICE_INDEX);
} else {
getConfig().put(ATTRIBUTE_CONSUMING_SERVICE_INDEX, String.valueOf(attributeConsumingServiceIndex));
}
}
@Override
public void validate(RealmModel realm) {
SslRequired sslRequired = realm.getSslRequired();

View file

@ -0,0 +1,98 @@
package org.keycloak.testsuite.broker;
import org.keycloak.broker.saml.SAMLIdentityProviderConfig;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.testsuite.saml.AbstractSamlTest;
import org.keycloak.testsuite.updaters.IdentityProviderAttributeUpdater;
import org.keycloak.testsuite.util.SamlClient;
import org.keycloak.testsuite.util.SamlClient.Binding;
import org.keycloak.testsuite.util.SamlClientBuilder;
import java.io.Closeable;
import org.junit.Assert;
import org.junit.Test;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import static org.keycloak.testsuite.broker.BrokerTestTools.getConsumerRoot;
/**
* Final class as it's not intended to be overriden.
*/
public final class KcSamlAttributeConsumingServiceIndexTest extends AbstractBrokerTest {
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return KcSamlBrokerConfiguration.INSTANCE;
}
@Test
public void testAttributeConsumingServiceIndexNotSet() throws Exception {
// No Attribute Consuming Service Index set -> No attribute added to AuthnRequest
try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource)
.update())
{
// Build the login request document
AuthnRequestType loginRep = SamlClient.createLoginRequestDocument(AbstractSamlTest.SAML_CLIENT_ID_SALES_POST + ".dot/ted", getConsumerRoot() + "/sales-post/saml", null);
Document doc = SAML2Request.convert(loginRep);
new SamlClientBuilder()
.authnRequest(getConsumerSamlEndpoint(bc.consumerRealmName()), doc, Binding.POST)
.build() // Request to consumer IdP
.login().idp(bc.getIDPAlias()).build()
.processSamlResponse(Binding.POST) // AuthnRequest to producer IdP
.targetAttributeSamlRequest()
.transformDocument((document) -> {
try
{
log.infof("Document: %s", DocumentUtil.asString(document));
// Find the AuthnRequest AttributeConsumingServiceIndex attribute
Node attrNode = document.getDocumentElement().getAttributes().getNamedItem("AttributeConsumingServiceIndex");
Assert.assertEquals("Unexpected AttributeConsumingServiceIndex attribute value", null, attrNode);
}
catch (Exception ex)
{
throw new RuntimeException(ex);
}
})
.build()
.execute();
}
}
@Test
public void testAttributeConsumingServiceIndexSet() throws Exception {
// Attribute Consuming Service Index set -> Attribute added to AuthnRequest
try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource)
.setAttribute(SAMLIdentityProviderConfig.ATTRIBUTE_CONSUMING_SERVICE_INDEX, "15")
.update())
{
// Build the login request document
AuthnRequestType loginRep = SamlClient.createLoginRequestDocument(AbstractSamlTest.SAML_CLIENT_ID_SALES_POST + ".dot/ted", getConsumerRoot() + "/sales-post/saml", null);
Document doc = SAML2Request.convert(loginRep);
new SamlClientBuilder()
.authnRequest(getConsumerSamlEndpoint(bc.consumerRealmName()), doc, Binding.POST)
.build() // Request to consumer IdP
.login().idp(bc.getIDPAlias()).build()
.processSamlResponse(Binding.POST) // AuthnRequest to producer IdP
.targetAttributeSamlRequest()
.transformDocument((document) -> {
try
{
log.infof("Document: %s", DocumentUtil.asString(document));
// Find the AuthnRequest AttributeConsumingServiceIndex attribute
String attrValue = document.getDocumentElement().getAttributes().getNamedItem("AttributeConsumingServiceIndex").getNodeValue();
Assert.assertEquals("Unexpected AttributeConsumingServiceIndex attribute value", "15", attrValue);
}
catch (Exception ex)
{
throw new RuntimeException(ex);
}
})
.build()
.execute();
}
}
}

View file

@ -718,6 +718,8 @@ identity-provider.saml.entity-id=Service Provider Entity ID
identity-provider.saml.entity-id.tooltip=The Entity ID that will be used to uniquely identify this SAML Service Provider
identity-provider.saml.protocol-endpoints.saml=SAML 2.0 Service Provider Metadata
identity-provider.saml.protocol-endpoints.saml.tooltip=Shows the configuration of the Service Provider endpoint
identity-provider.saml.attribute-consuming-service-index=Attribute Consuming Service Index
identity-provider.saml.attribute-consuming-service-index.tooltip=Index of the Attribute Consuming Service profile to request during authentication
saml-config=SAML Config
identity-provider.saml-config.tooltip=SAML SP and external IDP configuration.
single-signon-service-url=Single Sign-On Service URL

View file

@ -302,6 +302,13 @@
</div>
<kc-tooltip>{{:: 'identity-provider.allowed-clock-skew.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="attributeConsumingServiceIndex">{{:: 'identity-provider.saml.attribute-consuming-service-index' | translate}}</label>
<div class="col-md-6">
<input class="form-control" string-to-number type="number" min="0" max="65535" step="1" ng-model="identityProvider.config.attributeConsumingServiceIndex" id="attributeConsumingServiceIndex"/>
</div>
<kc-tooltip>{{:: 'identity-provider.saml.attribute-consuming-service-index.tooltip' | translate}}</kc-tooltip>
</div>
</fieldset>
<fieldset>
<legend collapsed><span class="text">{{:: 'identity-provider.saml.requested-authncontext' | translate}}</span> <kc-tooltip>{{:: 'identity-provider.saml.requested-authncontext.tooltip' | translate}}</kc-tooltip></legend>