From 67b2d5ffddf968a74948c084278f813660005a1c Mon Sep 17 00:00:00 2001 From: Luca Leonardo Scorcia Date: Sat, 25 Jul 2020 09:50:33 -0400 Subject: [PATCH] KEYCLOAK-14961 SAML Client: Add ability to request specific AuthnContexts to remote IdPs --- .../saml/SAML2AuthnRequestBuilder.java | 12 + .../SAML2RequestedAuthnContextBuilder.java | 66 +++++ .../saml/v2/writers/SAMLRequestWriter.java | 11 + .../broker/saml/SAMLIdentityProvider.java | 40 +++ .../saml/SAMLIdentityProviderConfig.java | 31 ++- ...KcSamlRequestedAuthnContextBrokerTest.java | 241 ++++++++++++++++++ .../messages/admin-messages_en.properties | 12 + .../admin/resources/js/controllers/realm.js | 29 +++ .../realm-identity-provider-saml.html | 66 ++++- 9 files changed, 505 insertions(+), 3 deletions(-) create mode 100644 saml-core/src/main/java/org/keycloak/saml/SAML2RequestedAuthnContextBuilder.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlRequestedAuthnContextBrokerTest.java diff --git a/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java b/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java index 11bb00b44b..59b2faa727 100755 --- a/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java +++ b/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java @@ -20,6 +20,7 @@ import org.keycloak.dom.saml.v2.assertion.NameIDType; import org.keycloak.dom.saml.v2.assertion.SubjectType; import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; import org.keycloak.dom.saml.v2.protocol.ExtensionsType; +import org.keycloak.dom.saml.v2.protocol.RequestedAuthnContextType; import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator; import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil; @@ -108,6 +109,17 @@ public class SAML2AuthnRequestBuilder implements SamlProtocolExtensionsAwareBuil return subject; } + public SAML2AuthnRequestBuilder requestedAuthnContext(SAML2RequestedAuthnContextBuilder requestedAuthnContextBuilder) { + RequestedAuthnContextType requestedAuthnContext = requestedAuthnContextBuilder.build(); + + // Only emit the RequestedAuthnContext element if at least a ClassRef or a DeclRef is present + if (!requestedAuthnContext.getAuthnContextClassRef().isEmpty() || + !requestedAuthnContext.getAuthnContextDeclRef().isEmpty()) + this.authnRequestType.setRequestedAuthnContext(requestedAuthnContext); + + return this; + } + public Document toDocument() { try { AuthnRequestType authnRequestType = createAuthnRequest(); diff --git a/saml-core/src/main/java/org/keycloak/saml/SAML2RequestedAuthnContextBuilder.java b/saml-core/src/main/java/org/keycloak/saml/SAML2RequestedAuthnContextBuilder.java new file mode 100644 index 0000000000..2e180b86f8 --- /dev/null +++ b/saml-core/src/main/java/org/keycloak/saml/SAML2RequestedAuthnContextBuilder.java @@ -0,0 +1,66 @@ +/* + * Copyright 2016 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.saml; + +import org.keycloak.dom.saml.v2.protocol.AuthnContextComparisonType; +import org.keycloak.dom.saml.v2.protocol.RequestedAuthnContextType; + +import java.util.LinkedList; +import java.util.List; + +public class SAML2RequestedAuthnContextBuilder { + private final RequestedAuthnContextType requestedAuthnContextType; + private AuthnContextComparisonType comparison; + private List requestedAuthnContextClassRefList; + private List requestedAuthnContextDeclRefList; + + public SAML2RequestedAuthnContextBuilder() { + this.requestedAuthnContextType = new RequestedAuthnContextType(); + this.requestedAuthnContextClassRefList = new LinkedList(); + this.requestedAuthnContextDeclRefList = new LinkedList(); + } + + public SAML2RequestedAuthnContextBuilder setComparison(AuthnContextComparisonType comparison) { + this.comparison = comparison; + return this; + } + + public SAML2RequestedAuthnContextBuilder addAuthnContextClassRef(String authnContextClassRef) { + this.requestedAuthnContextClassRefList.add(authnContextClassRef); + return this; + } + + public SAML2RequestedAuthnContextBuilder addAuthnContextDeclRef(String authnContextDeclRef) { + this.requestedAuthnContextDeclRefList.add(authnContextDeclRef); + return this; + } + + public RequestedAuthnContextType build() { + if (this.comparison != null) + this.requestedAuthnContextType.setComparison(this.comparison); + + for (String requestedAuthnContextClassRef: this.requestedAuthnContextClassRefList) + if (requestedAuthnContextClassRef != null && !requestedAuthnContextClassRef.isEmpty()) + this.requestedAuthnContextType.addAuthnContextClassRef(requestedAuthnContextClassRef); + + for (String requestedAuthnContextDeclRef: this.requestedAuthnContextDeclRefList) + if (requestedAuthnContextDeclRef != null && !requestedAuthnContextDeclRef.isEmpty()) + this.requestedAuthnContextType.addAuthnContextDeclRef(requestedAuthnContextDeclRef); + + return this.requestedAuthnContextType; + } +} \ No newline at end of file diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLRequestWriter.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLRequestWriter.java index 72e5cfae7c..9f13d6535a 100755 --- a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLRequestWriter.java +++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLRequestWriter.java @@ -270,6 +270,17 @@ public class SAMLRequestWriter extends BaseWriter { } } + List authnContextDeclRef = requestedAuthnContextType.getAuthnContextDeclRef(); + + if (authnContextDeclRef != null && !authnContextDeclRef.isEmpty()) { + for (String declRef : authnContextDeclRef) { + StaxUtil.writeStartElement(writer, ASSERTION_PREFIX, JBossSAMLConstants.AUTHN_CONTEXT_DECL_REF.get(), ASSERTION_NSURI.get()); + StaxUtil.writeNameSpace(writer, ASSERTION_PREFIX, ASSERTION_NSURI.get()); + StaxUtil.writeCharacters(writer, declRef); + StaxUtil.writeEndElement(writer); + } + } + StaxUtil.writeEndElement(writer); StaxUtil.flush(writer); } diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java index b1a7567a58..61dd5afee1 100755 --- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java +++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java @@ -45,6 +45,7 @@ import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator; import org.keycloak.saml.validators.DestinationValidator; import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.util.JsonSerialization; import org.w3c.dom.Element; @@ -54,6 +55,8 @@ import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import java.net.URI; import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; import java.util.Iterator; import java.util.List; import java.util.Set; @@ -97,6 +100,16 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider getAuthnContextClassRefUris() { + String authnContextClassRefs = getConfig().getAuthnContextClassRefs(); + if (authnContextClassRefs == null || authnContextClassRefs.isEmpty()) + return new LinkedList(); + + try { + return Arrays.asList(JsonSerialization.readValue(authnContextClassRefs, String[].class)); + } catch (Exception e) { + logger.warn("Could not json-deserialize AuthContextClassRefs config entry: " + authnContextClassRefs, e); + return new LinkedList(); + } + } + + private List getAuthnContextDeclRefUris() { + String authnContextDeclRefs = getConfig().getAuthnContextDeclRefs(); + if (authnContextDeclRefs == null || authnContextDeclRefs.isEmpty()) + return new LinkedList(); + + try { + return Arrays.asList(JsonSerialization.readValue(authnContextDeclRefs, String[].class)); + } catch (Exception e) { + logger.warn("Could not json-deserialize AuthContextDeclRefs config entry: " + authnContextDeclRefs, e); + return new LinkedList(); + } + } + @Override public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) { ResponseType responseType = (ResponseType)context.getContextData().get(SAMLEndpoint.SAML_LOGIN_RESPONSE); diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java index 0c36fad772..db954fb183 100755 --- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java +++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java @@ -19,8 +19,8 @@ package org.keycloak.broker.saml; import static org.keycloak.common.util.UriUtils.checkUrl; import org.keycloak.common.enums.SslRequired; +import org.keycloak.dom.saml.v2.protocol.AuthnContextComparisonType; import org.keycloak.models.IdentityProviderModel; - import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.protocol.saml.SamlPrincipalType; @@ -53,6 +53,9 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel { public static final String WANT_AUTHN_REQUESTS_SIGNED = "wantAuthnRequestsSigned"; public static final String XML_SIG_KEY_INFO_KEY_NAME_TRANSFORMER = "xmlSigKeyInfoKeyNameTransformer"; public static final String ENABLED_FROM_METADATA = "enabledFromMetadata"; + public static final String AUTHN_CONTEXT_COMPARISON_TYPE = "authnContextComparisonType"; + public static final String AUTHN_CONTEXT_CLASS_REFS = "authnContextClassRefs"; + public static final String AUTHN_CONTEXT_DECL_REFS = "authnContextDeclRefs"; public SAMLIdentityProviderConfig() { } @@ -281,7 +284,7 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel { public void setPrincipalAttribute(String principalAttribute) { getConfig().put(PRINCIPAL_ATTRIBUTE, principalAttribute); } - + public boolean isEnabledFromMetadata() { return Boolean.valueOf(getConfig().get(ENABLED_FROM_METADATA )); } @@ -290,6 +293,30 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel { getConfig().put(ENABLED_FROM_METADATA , String.valueOf(enabled)); } + public AuthnContextComparisonType getAuthnContextComparisonType() { + return AuthnContextComparisonType.fromValue(getConfig().getOrDefault(AUTHN_CONTEXT_COMPARISON_TYPE, AuthnContextComparisonType.EXACT.value())); + } + + public void setAuthnContextComparisonType(AuthnContextComparisonType authnContextComparisonType) { + getConfig().put(AUTHN_CONTEXT_COMPARISON_TYPE, authnContextComparisonType.value()); + } + + public String getAuthnContextClassRefs() { + return getConfig().get(AUTHN_CONTEXT_CLASS_REFS); + } + + public void setAuthnContextClassRefs(String authnContextClassRefs) { + getConfig().put(AUTHN_CONTEXT_CLASS_REFS, authnContextClassRefs); + } + + public String getAuthnContextDeclRefs() { + return getConfig().get(AUTHN_CONTEXT_DECL_REFS); + } + + public void setAuthnContextDeclRefs(String authnContextDeclRefs) { + getConfig().put(AUTHN_CONTEXT_DECL_REFS, authnContextDeclRefs); + } + @Override public void validate(RealmModel realm) { SslRequired sslRequired = realm.getSslRequired(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlRequestedAuthnContextBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlRequestedAuthnContextBrokerTest.java new file mode 100644 index 0000000000..a1a648000d --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlRequestedAuthnContextBrokerTest.java @@ -0,0 +1,241 @@ +package org.keycloak.testsuite.broker; + +import org.keycloak.broker.saml.SAMLIdentityProviderConfig; +import org.keycloak.dom.saml.v2.protocol.AuthnContextComparisonType; +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.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import static org.junit.Assert.assertThat; +import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.AC_PASSWORD_PROTECTED_TRANSPORT; +import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.ASSERTION_NSURI; +import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.PROTOCOL_NSURI; +import static org.keycloak.testsuite.broker.BrokerTestTools.getConsumerRoot; + +/** + * Final class as it's not intended to be overriden. + */ +public final class KcSamlRequestedAuthnContextBrokerTest extends AbstractBrokerTest { + + @Override + protected BrokerConfiguration getBrokerConfiguration() { + return KcSamlBrokerConfiguration.INSTANCE; + } + + @Test + public void testNoComparisonTypeNoClassRefsAndNoDeclRefs() throws Exception { + // No comparison type, no classrefs, no declrefs -> No RequestedAuthnContext + 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 RequestedAuthnContext element + Element requestedAuthnContextElement = DocumentUtil.getDirectChildElement(document.getDocumentElement(), PROTOCOL_NSURI.get(), "RequestedAuthnContext"); + Assert.assertThat("RequestedAuthnContext element found in request document, but was not necessary as ClassRef/DeclRefs were not specified", requestedAuthnContextElement, Matchers.nullValue()); + } + catch (Exception ex) + { + throw new RuntimeException(ex); + } + }) + .build() + .execute(); + } + } + + @Test + public void testComparisonTypeSetNoClassRefsAndNoDeclRefs() throws Exception { + // Comparison type set, no classrefs, no declrefs -> No RequestedAuthnContext + try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource) + .setAttribute(SAMLIdentityProviderConfig.AUTHN_CONTEXT_COMPARISON_TYPE, AuthnContextComparisonType.MINIMUM.value()) + .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 RequestedAuthnContext element + Element requestedAuthnContextElement = DocumentUtil.getDirectChildElement(document.getDocumentElement(), PROTOCOL_NSURI.get(), "RequestedAuthnContext"); + Assert.assertThat("RequestedAuthnContext element found in request document, but was not necessary as ClassRef/DeclRefs were not specified", requestedAuthnContextElement, Matchers.nullValue()); + } + catch (Exception ex) + { + throw new RuntimeException(ex); + } + }) + .build() + .execute(); + } + } + + @Test + public void testComparisonTypeSetClassRefsSetNoDeclRefs() throws Exception { + // Comparison type set, classref present, no declrefs -> RequestedAuthnContext with AuthnContextClassRef + try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource) + .setAttribute(SAMLIdentityProviderConfig.AUTHN_CONTEXT_COMPARISON_TYPE, AuthnContextComparisonType.EXACT.value()) + .setAttribute(SAMLIdentityProviderConfig.AUTHN_CONTEXT_CLASS_REFS, "[\"" + AC_PASSWORD_PROTECTED_TRANSPORT.get() + "\"]") + .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 RequestedAuthnContext element + Element requestedAuthnContextElement = DocumentUtil.getDirectChildElement(document.getDocumentElement(), PROTOCOL_NSURI.get(), "RequestedAuthnContext"); + Assert.assertThat("RequestedAuthnContext element not found in request document", requestedAuthnContextElement, Matchers.notNullValue()); + + // Verify the ComparisonType attribute + Assert.assertThat("RequestedAuthnContext element not found in request document", requestedAuthnContextElement.getAttribute("Comparison"), Matchers.is(AuthnContextComparisonType.EXACT.value())); + + // Find the RequestedAuthnContext/ClassRef element + Element requestedAuthnContextClassRefElement = DocumentUtil.getDirectChildElement(requestedAuthnContextElement, ASSERTION_NSURI.get(), "AuthnContextClassRef"); + Assert.assertThat("RequestedAuthnContext/AuthnContextClassRef element not found in request document", requestedAuthnContextClassRefElement, Matchers.notNullValue()); + + // Make sure the RequestedAuthnContext/ClassRef element has the requested value + Assert.assertThat("RequestedAuthnContext/AuthnContextClassRef element does not have the expected value", requestedAuthnContextClassRefElement.getTextContent(), Matchers.is(AC_PASSWORD_PROTECTED_TRANSPORT.get())); + } + catch (Exception ex) + { + throw new RuntimeException(ex); + } + }) + .build() + .execute(); + } + } + + @Test + public void testComparisonTypeSetNoClassRefsDeclRefsSet() throws Exception { + // Comparison type set, no classref present, declrefs set -> RequestedAuthnContext with AuthnContextDeclRef + try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource) + .setAttribute(SAMLIdentityProviderConfig.AUTHN_CONTEXT_COMPARISON_TYPE, AuthnContextComparisonType.MINIMUM.value()) + .setAttribute(SAMLIdentityProviderConfig.AUTHN_CONTEXT_DECL_REFS, "[\"secure/name/password/icmaolr/uri\"]") + .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 RequestedAuthnContext element + Element requestedAuthnContextElement = DocumentUtil.getDirectChildElement(document.getDocumentElement(), PROTOCOL_NSURI.get(), "RequestedAuthnContext"); + Assert.assertThat("RequestedAuthnContext element not found in request document", requestedAuthnContextElement, Matchers.notNullValue()); + + // Verify the ComparisonType attribute + Assert.assertThat("RequestedAuthnContext element not found in request document", requestedAuthnContextElement.getAttribute("Comparison"), Matchers.is(AuthnContextComparisonType.MINIMUM.value())); + + // Find the RequestedAuthnContext/DeclRef element + Element requestedAuthnContextDeclRefElement = DocumentUtil.getDirectChildElement(requestedAuthnContextElement, ASSERTION_NSURI.get(), "AuthnContextDeclRef"); + Assert.assertThat("RequestedAuthnContext/AuthnContextDeclRef element not found in request document", requestedAuthnContextDeclRefElement, Matchers.notNullValue()); + + // Make sure the RequestedAuthnContext/DeclRef element has the requested value + Assert.assertThat("RequestedAuthnContext/AuthnContextDeclRef element does not have the expected value", requestedAuthnContextDeclRefElement.getTextContent(), Matchers.is("secure/name/password/icmaolr/uri")); + } + catch (Exception ex) + { + throw new RuntimeException(ex); + } + }) + .build() + .execute(); + } + } + + @Test + public void testNoComparisonTypeClassRefsSetNoDeclRefs() throws Exception { + // Comparison type set, no classref present, declrefs set -> RequestedAuthnContext with comparison Exact and AuthnContextClassRef + try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource) + .setAttribute(SAMLIdentityProviderConfig.AUTHN_CONTEXT_CLASS_REFS, "[\"" + AC_PASSWORD_PROTECTED_TRANSPORT.get() + "\"]") + .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 RequestedAuthnContext element + Element requestedAuthnContextElement = DocumentUtil.getDirectChildElement(document.getDocumentElement(), PROTOCOL_NSURI.get(), "RequestedAuthnContext"); + Assert.assertThat("RequestedAuthnContext element not found in request document", requestedAuthnContextElement, Matchers.notNullValue()); + + // Verify the ComparisonType attribute + Assert.assertThat("RequestedAuthnContext element not found in request document", requestedAuthnContextElement.getAttribute("Comparison"), Matchers.is(AuthnContextComparisonType.EXACT.value())); + + // Find the RequestedAuthnContext/ClassRef element + Element requestedAuthnContextClassRefElement = DocumentUtil.getDirectChildElement(requestedAuthnContextElement, ASSERTION_NSURI.get(), "AuthnContextClassRef"); + Assert.assertThat("RequestedAuthnContext/AuthnContextClassRef element not found in request document", requestedAuthnContextClassRefElement, Matchers.notNullValue()); + + // Make sure the RequestedAuthnContext/ClassRef element has the requested value + Assert.assertThat("RequestedAuthnContext/AuthnContextClassRef element does not have the expected value", requestedAuthnContextClassRefElement.getTextContent(), Matchers.is(AC_PASSWORD_PROTECTED_TRANSPORT.get())); + } + catch (Exception ex) + { + throw new RuntimeException(ex); + } + }) + .build() + .execute(); + } + } +} diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index ed65e8ddb5..f3c7b59902 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -699,6 +699,18 @@ validating-x509-certificate.tooltip=The certificate in PEM format that must be u saml.loginHint=Pass subject saml.loginHint.tooltip=During login phase, forward an optional login_hint query parameter to SAML AuthnRequest's Subject. saml.import-from-url.tooltip=Import metadata from a remote IDP SAML entity descriptor. +identity-provider.saml.requested-authncontext=Requested AuthnContext Constraints +identity-provider.saml.requested-authncontext.tooltip=Allows the SP to specify the authentication context requirements of authentication statements returned. +identity-provider.saml.authncontext-comparison-type=Comparison +identity-provider.saml.authncontext-comparison-type.tooltip=Specifies the comparison method used to evaluate the requested context classes or statements. The default is "Exact". +identity-provider.saml.authncontext-comparison-type.exact=Exact +identity-provider.saml.authncontext-comparison-type.minimum=Minimum +identity-provider.saml.authncontext-comparison-type.maximum=Maximum +identity-provider.saml.authncontext-comparison-type.better=Better +identity-provider.saml.authncontext-class-ref=AuthnContext ClassRefs +identity-provider.saml.authncontext-class-ref.tooltip=Ordered list of requested AuthnContext ClassRefs. +identity-provider.saml.authncontext-decl-ref=AuthnContext DeclRefs +identity-provider.saml.authncontext-decl-ref.tooltip=Ordered list of requested AuthnContext DeclRefs. social.client-id.tooltip=The client identifier registered with the identity provider. social.client-secret.tooltip=The client secret registered with the identity provider. This field is able to obtain its value from vault, use ${vault.ID} format. social.default-scopes.tooltip=The scopes to be sent when asking for authorization. See the documentation for possible values, separator and default value'. diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index 83e7799020..1c59452558 100644 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -1125,6 +1125,35 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload } }; + if (instance && instance.alias) { + try { $scope.authnContextClassRefs = JSON.parse($scope.identityProvider.config.authnContextClassRefs || '[]'); } catch (e) { $scope.authnContextClassRefs = []; } + try { $scope.authnContextDeclRefs = JSON.parse($scope.identityProvider.config.authnContextDeclRefs || '[]'); } catch (e) { $scope.authnContextDeclRefs = []; } + } else { + $scope.authnContextClassRefs = []; + $scope.authnContextDeclRefs = []; + } + + $scope.deleteAuthnContextClassRef = function(index) { + $scope.authnContextClassRefs.splice(index, 1); + $scope.identityProvider.config.authnContextClassRefs = JSON.stringify($scope.authnContextClassRefs); + }; + + $scope.addAuthnContextClassRef = function() { + $scope.authnContextClassRefs.push($scope.newAuthnContextClassRef); + $scope.identityProvider.config.authnContextClassRefs = JSON.stringify($scope.authnContextClassRefs); + $scope.newAuthnContextClassRef = ""; + }; + + $scope.deleteAuthnContextDeclRef = function(index) { + $scope.authnContextDeclRefs.splice(index, 1); + $scope.identityProvider.config.authnContextDeclRefs = JSON.stringify($scope.authnContextDeclRefs); + }; + + $scope.addAuthnContextDeclRef = function() { + $scope.authnContextDeclRefs.push($scope.newAuthnContextDeclRef); + $scope.identityProvider.config.authnContextDeclRefs = JSON.stringify($scope.authnContextDeclRefs); + $scope.newAuthnContextDeclRef = ""; + }; }); module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http, $location, $route, Dialog, Notifications, TimeUnit, TimeUnit2, serverInfo) { diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html index 101c3bc192..36d81c7f07 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html @@ -281,7 +281,71 @@ {{:: 'identity-provider.allowed-clock-skew.tooltip' | translate}} - + +
+ {{:: 'identity-provider.saml.requested-authncontext' | translate}} {{:: 'identity-provider.saml.requested-authncontext.tooltip' | translate}} + +
+ +
+
+ +
+
+ {{:: 'identity-provider.saml.authncontext-comparison-type.tooltip' | translate}} +
+
+ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ {{:: 'identity-provider.saml.authncontext-class-ref.tooltip' | translate}} +
+
+ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ {{:: 'identity-provider.saml.authncontext-decl-ref.tooltip' | translate}} +
+
{{:: 'import-external-idp-config' | translate}} {{:: 'import-external-idp-config.tooltip' | translate}}