KEYCLOAK-14961 SAML Client: Add ability to request specific AuthnContexts to remote IdPs
This commit is contained in:
parent
1c4a2db8e1
commit
67b2d5ffdd
9 changed files with 505 additions and 3 deletions
|
@ -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();
|
||||
|
|
|
@ -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<String> requestedAuthnContextClassRefList;
|
||||
private List<String> requestedAuthnContextDeclRefList;
|
||||
|
||||
public SAML2RequestedAuthnContextBuilder() {
|
||||
this.requestedAuthnContextType = new RequestedAuthnContextType();
|
||||
this.requestedAuthnContextClassRefList = new LinkedList<String>();
|
||||
this.requestedAuthnContextDeclRefList = new LinkedList<String>();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -270,6 +270,17 @@ public class SAMLRequestWriter extends BaseWriter {
|
|||
}
|
||||
}
|
||||
|
||||
List<String> 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);
|
||||
}
|
||||
|
|
|
@ -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<SAMLIdentityP
|
|||
protocolBinding = JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get();
|
||||
}
|
||||
|
||||
SAML2RequestedAuthnContextBuilder requestedAuthnContext =
|
||||
new SAML2RequestedAuthnContextBuilder()
|
||||
.setComparison(getConfig().getAuthnContextComparisonType());
|
||||
|
||||
for (String authnContextClassRef : getAuthnContextClassRefUris())
|
||||
requestedAuthnContext.addAuthnContextClassRef(authnContextClassRef);
|
||||
|
||||
for (String authnContextDeclRef : getAuthnContextDeclRefUris())
|
||||
requestedAuthnContext.addAuthnContextDeclRef(authnContextDeclRef);
|
||||
|
||||
String loginHint = getConfig().isLoginHint() ? request.getAuthenticationSession().getClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM) : null;
|
||||
SAML2AuthnRequestBuilder authnRequestBuilder = new SAML2AuthnRequestBuilder()
|
||||
.assertionConsumerUrl(assertionConsumerServiceUrl)
|
||||
|
@ -107,6 +120,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
|||
.nameIdPolicy(SAML2NameIDPolicyBuilder
|
||||
.format(nameIDPolicyFormat)
|
||||
.setAllowCreate(Boolean.TRUE))
|
||||
.requestedAuthnContext(requestedAuthnContext)
|
||||
.subject(loginHint);
|
||||
|
||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(session)
|
||||
|
@ -148,6 +162,32 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
|||
return UriBuilder.fromUri(uriInfo.getBaseUri()).path("realms").path(realm.getName()).build().toString();
|
||||
}
|
||||
|
||||
private List<String> getAuthnContextClassRefUris() {
|
||||
String authnContextClassRefs = getConfig().getAuthnContextClassRefs();
|
||||
if (authnContextClassRefs == null || authnContextClassRefs.isEmpty())
|
||||
return new LinkedList<String>();
|
||||
|
||||
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<String>();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> getAuthnContextDeclRefUris() {
|
||||
String authnContextDeclRefs = getConfig().getAuthnContextDeclRefs();
|
||||
if (authnContextDeclRefs == null || authnContextDeclRefs.isEmpty())
|
||||
return new LinkedList<String>();
|
||||
|
||||
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<String>();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {
|
||||
ResponseType responseType = (ResponseType)context.getContextData().get(SAMLEndpoint.SAML_LOGIN_RESPONSE);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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'.
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -281,7 +281,71 @@
|
|||
</div>
|
||||
<kc-tooltip>{{:: 'identity-provider.allowed-clock-skew.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
</fieldset>
|
||||
</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>
|
||||
|
||||
<div class="form-group clearfix">
|
||||
<label class="col-md-2 control-label" for="authnContextComparisonType">{{:: 'identity-provider.saml.authncontext-comparison-type' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<select class="form-control" id="authnContextComparisonType"
|
||||
ng-init="identityProvider.config.authnContextComparisonType = identityProvider.config.authnContextComparisonType || 'exact'"
|
||||
ng-model="identityProvider.config.authnContextComparisonType">
|
||||
<option value="exact">{{:: 'identity-provider.saml.authncontext-comparison-type.exact' | translate}}</option>
|
||||
<option value="minimum">{{:: 'identity-provider.saml.authncontext-comparison-type.minimum' | translate}}</option>
|
||||
<option value="maximum">{{:: 'identity-provider.saml.authncontext-comparison-type.maximum' | translate}}</option>
|
||||
<option value="better">{{:: 'identity-provider.saml.authncontext-comparison-type.better' | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'identity-provider.saml.authncontext-comparison-type.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="type" class="col-md-2 control-label">{{:: 'identity-provider.saml.authncontext-class-ref' | translate}}</label>
|
||||
<div class="col-sm-4">
|
||||
<div class="input-group" ng-repeat="(i, authnContextClassRef) in authnContextClassRefs track by $index">
|
||||
<input class="form-control" ng-model="authnContextClassRefs[i]">
|
||||
<div class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" data-ng-click="deleteAuthnContextClassRef($index)">
|
||||
<span class="fa fa-minus"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class = "input-group">
|
||||
<input class="form-control" ng-model="newAuthnContextClassRef" id="newAuthnContextClassRef">
|
||||
<div class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" data-ng-click="newAuthnContextClassRef.length > 0 && addAuthnContextClassRef()">
|
||||
<span class="fa fa-plus"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'identity-provider.saml.authncontext-class-ref.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="type" class="col-md-2 control-label">{{:: 'identity-provider.saml.authncontext-decl-ref' | translate}}</label>
|
||||
<div class="col-sm-4">
|
||||
<div class="input-group" ng-repeat="(i, authnContextDeclRef) in authnContextDeclRefs track by $index">
|
||||
<input class="form-control" ng-model="authnContextDeclRefs[i]">
|
||||
<div class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" data-ng-click="deleteAuthnContextDeclRef($index)">
|
||||
<span class="fa fa-minus"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class = "input-group">
|
||||
<input class="form-control" ng-model="newAuthnContextDeclRef" id="newAuthnContextDeclRef">
|
||||
<div class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" data-ng-click="newAuthnContextDeclRef.length > 0 && addAuthnContextDeclRef()">
|
||||
<span class="fa fa-plus"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'identity-provider.saml.authncontext-decl-ref.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset data-ng-show="newIdentityProvider">
|
||||
<legend uncollapsed><span class="text">{{:: 'import-external-idp-config' | translate}}</span> <kc-tooltip>{{:: 'import-external-idp-config.tooltip' | translate}}</kc-tooltip></legend>
|
||||
<div class="form-group" data-ng-show="newIdentityProvider">
|
||||
|
|
Loading…
Reference in a new issue