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.assertion.SubjectType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
|
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.api.saml.v2.request.SAML2Request;
|
||||||
import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator;
|
import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator;
|
||||||
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
|
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
|
||||||
|
@ -108,6 +109,17 @@ public class SAML2AuthnRequestBuilder implements SamlProtocolExtensionsAwareBuil
|
||||||
return subject;
|
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() {
|
public Document toDocument() {
|
||||||
try {
|
try {
|
||||||
AuthnRequestType authnRequestType = createAuthnRequest();
|
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.writeEndElement(writer);
|
||||||
StaxUtil.flush(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.processing.core.util.KeycloakKeySamlExtensionGenerator;
|
||||||
import org.keycloak.saml.validators.DestinationValidator;
|
import org.keycloak.saml.validators.DestinationValidator;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
import org.w3c.dom.Element;
|
import org.w3c.dom.Element;
|
||||||
|
|
||||||
|
@ -54,6 +55,8 @@ import javax.ws.rs.core.UriBuilder;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -97,6 +100,16 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
||||||
protocolBinding = JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get();
|
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;
|
String loginHint = getConfig().isLoginHint() ? request.getAuthenticationSession().getClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM) : null;
|
||||||
SAML2AuthnRequestBuilder authnRequestBuilder = new SAML2AuthnRequestBuilder()
|
SAML2AuthnRequestBuilder authnRequestBuilder = new SAML2AuthnRequestBuilder()
|
||||||
.assertionConsumerUrl(assertionConsumerServiceUrl)
|
.assertionConsumerUrl(assertionConsumerServiceUrl)
|
||||||
|
@ -107,6 +120,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
||||||
.nameIdPolicy(SAML2NameIDPolicyBuilder
|
.nameIdPolicy(SAML2NameIDPolicyBuilder
|
||||||
.format(nameIDPolicyFormat)
|
.format(nameIDPolicyFormat)
|
||||||
.setAllowCreate(Boolean.TRUE))
|
.setAllowCreate(Boolean.TRUE))
|
||||||
|
.requestedAuthnContext(requestedAuthnContext)
|
||||||
.subject(loginHint);
|
.subject(loginHint);
|
||||||
|
|
||||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(session)
|
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();
|
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
|
@Override
|
||||||
public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {
|
public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {
|
||||||
ResponseType responseType = (ResponseType)context.getContextData().get(SAMLEndpoint.SAML_LOGIN_RESPONSE);
|
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 static org.keycloak.common.util.UriUtils.checkUrl;
|
||||||
|
|
||||||
import org.keycloak.common.enums.SslRequired;
|
import org.keycloak.common.enums.SslRequired;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.AuthnContextComparisonType;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.protocol.saml.SamlPrincipalType;
|
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 WANT_AUTHN_REQUESTS_SIGNED = "wantAuthnRequestsSigned";
|
||||||
public static final String XML_SIG_KEY_INFO_KEY_NAME_TRANSFORMER = "xmlSigKeyInfoKeyNameTransformer";
|
public static final String XML_SIG_KEY_INFO_KEY_NAME_TRANSFORMER = "xmlSigKeyInfoKeyNameTransformer";
|
||||||
public static final String ENABLED_FROM_METADATA = "enabledFromMetadata";
|
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() {
|
public SAMLIdentityProviderConfig() {
|
||||||
}
|
}
|
||||||
|
@ -281,7 +284,7 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
|
||||||
public void setPrincipalAttribute(String principalAttribute) {
|
public void setPrincipalAttribute(String principalAttribute) {
|
||||||
getConfig().put(PRINCIPAL_ATTRIBUTE, principalAttribute);
|
getConfig().put(PRINCIPAL_ATTRIBUTE, principalAttribute);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEnabledFromMetadata() {
|
public boolean isEnabledFromMetadata() {
|
||||||
return Boolean.valueOf(getConfig().get(ENABLED_FROM_METADATA ));
|
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));
|
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
|
@Override
|
||||||
public void validate(RealmModel realm) {
|
public void validate(RealmModel realm) {
|
||||||
SslRequired sslRequired = realm.getSslRequired();
|
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=Pass subject
|
||||||
saml.loginHint.tooltip=During login phase, forward an optional login_hint query parameter to SAML AuthnRequest's 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.
|
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-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.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'.
|
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) {
|
module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http, $location, $route, Dialog, Notifications, TimeUnit, TimeUnit2, serverInfo) {
|
||||||
|
|
|
@ -281,7 +281,71 @@
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>{{:: 'identity-provider.allowed-clock-skew.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'identity-provider.allowed-clock-skew.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</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">
|
<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>
|
<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">
|
<div class="form-group" data-ng-show="newIdentityProvider">
|
||||||
|
|
Loading…
Reference in a new issue