From 9858f6a7d7e1bd5e2f9983cbdf4f152aa6db73bc Mon Sep 17 00:00:00 2001 From: Vladimir Schafer Date: Sat, 26 Mar 2011 11:12:38 +0200 Subject: [PATCH] SES-82 Adding support for ECP profile. Proxy count can now be configured in the WebSSOProfileOptions. List of allowed IDPs can now be configured in the WebSSOProfileOptions. --- spring-security-saml/pom.xml | 3 + spring-security-saml/saml2-core/pom.xml | 20 +++ .../security/saml/SAMLBootstrap.java | 22 ++- .../security/saml/SAMLConstants.java | 3 + .../security/saml/SAMLEntryPoint.java | 54 ++++++- .../saml/metadata/MetadataGenerator.java | 8 +- .../saml/processor/HTTPPAOS11Binding.java | 52 +++++++ .../saml/websso/AbstractProfileBase.java | 23 ++- .../saml/websso/SingleLogoutProfileImpl.java | 4 +- .../saml/websso/WebSSOProfileECPImpl.java | 146 ++++++++++++++++++ .../saml/websso/WebSSOProfileImpl.java | 109 ++++++++----- .../saml/websso/WebSSOProfileOptions.java | 86 +++++++++-- .../security/saml/SAMLEntryPointTest.java | 20 ++- .../saml/websso/WebSSOProfileImplTest.java | 6 +- spring-security-saml/saml2-sample/pom.xml | 6 + .../resources/security/securityContext.xml | 34 ++-- .../saml2-sample/src/main/webapp/index.jsp | 21 +++ 17 files changed, 527 insertions(+), 90 deletions(-) create mode 100755 spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/processor/HTTPPAOS11Binding.java create mode 100755 spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/websso/WebSSOProfileECPImpl.java diff --git a/spring-security-saml/pom.xml b/spring-security-saml/pom.xml index 2b796f0..7b00b82 100644 --- a/spring-security-saml/pom.xml +++ b/spring-security-saml/pom.xml @@ -53,6 +53,9 @@ Rob Moore + + Jonathan Tellier + diff --git a/spring-security-saml/saml2-core/pom.xml b/spring-security-saml/saml2-core/pom.xml index 56f60f5..51d33e2 100644 --- a/spring-security-saml/saml2-core/pom.xml +++ b/spring-security-saml/saml2-core/pom.xml @@ -76,6 +76,26 @@ 4.4 test + + + xmlunit + xmlunit + 1.0 + test + + + + org.springframework + spring-mock + 2.0.8 + test + + + commons-logging + commons-logging + + + diff --git a/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/SAMLBootstrap.java b/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/SAMLBootstrap.java index c3c9309..7328747 100755 --- a/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/SAMLBootstrap.java +++ b/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/SAMLBootstrap.java @@ -1,6 +1,20 @@ +/* Copyright 2011 Vladimir Schaefer + * + * 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.springframework.security.saml; -import org.opensaml.DefaultBootstrap; +import org.opensaml.PaosBootstrap; import org.opensaml.xml.ConfigurationException; import org.opensaml.xml.parse.ParserPool; import org.springframework.beans.BeansException; @@ -11,6 +25,8 @@ /** * Initialization for SAML library. Is automatically called as part of Spring initialization. + * + * @author Vladimir Schaefer */ public class SAMLBootstrap implements BeanFactoryPostProcessor { @@ -22,7 +38,7 @@ public class SAMLBootstrap implements BeanFactoryPostProcessor { */ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { try { - DefaultBootstrap.bootstrap(); + PaosBootstrap.bootstrap(); ParserPool pool = beanFactory.getBean(ParserPool.class); new ParserPoolHolder(pool); } catch (ConfigurationException e) { @@ -30,4 +46,4 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) } } -} \ No newline at end of file +} diff --git a/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/SAMLConstants.java b/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/SAMLConstants.java index 3dc8b14..dc1fc35 100755 --- a/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/SAMLConstants.java +++ b/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/SAMLConstants.java @@ -28,5 +28,8 @@ public class SAMLConstants { public static final String SUCCESS = "SUCCESS"; public static final String FAILURE = "FAILURE"; + + public static final String PAOS_HTTP_ACCEPT_HEADER = "application/vnd.paos+xml"; + public static final String PAOS_HTTP_HEADER = "PAOS"; } diff --git a/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/SAMLEntryPoint.java b/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/SAMLEntryPoint.java index ec36ad5..349405d 100644 --- a/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/SAMLEntryPoint.java +++ b/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/SAMLEntryPoint.java @@ -61,6 +61,7 @@ public class SAMLEntryPoint extends GenericFilterBean implements AuthenticationE protected String idpSelectionPath; protected WebSSOProfileOptions defaultOptions; protected WebSSOProfile webSSOprofile; + protected WebSSOProfile webSSOprofileECP; protected MetadataManager metadata; protected SAMLLogger samlLogger; protected SAMLContextProvider contextProvider; @@ -130,14 +131,30 @@ public void commence(HttpServletRequest request, HttpServletResponse response, A try { - if (idpSelectionPath != null && !isLoginRequest(request)) { + boolean ecpRequest = isECPRequest(request); + + if (!ecpRequest && idpSelectionPath != null && !isLoginRequest(request)) { + request.getRequestDispatcher(idpSelectionPath).include(request, response); + } else { + SAMLMessageContext context = contextProvider.getLocalEntity(request, response); SAMLMessageStorage storage = new HttpSessionStorage(request); - WebSSOProfileOptions options = getProfileOptions(request, response, e); - webSSOprofile.sendAuthenticationRequest(context, options, storage); + WebSSOProfileOptions options = getProfileOptions(request, response, context, e); + + if (ecpRequest) { + if (webSSOprofileECP == null) { + throw new ServletException("ECP profile isn't available in the entry point, check your configuration"); + } else { + webSSOprofileECP.sendAuthenticationRequest(context, options, storage); + } + } else { + webSSOprofile.sendAuthenticationRequest(context, options, storage); + } + samlLogger.log(SAMLConstants.AUTH_N_REQUEST, SAMLConstants.SUCCESS, context, e); + } } catch (SAMLException e1) { @@ -150,6 +167,24 @@ public void commence(HttpServletRequest request, HttpServletResponse response, A } + /** + * Analyzes the request headers in order to determine if it comes from an ECP-enabled + * client and based on this decides whether ECP profile will be used. Subclasses can override + * the method to control when is the ECP invoked. + * + * @param request request to analyze + * @return whether the request comes from an ECP-enabled client or not + */ + protected boolean isECPRequest(HttpServletRequest request) { + String acceptHeader = request.getHeader("Accept"); + + return acceptHeader != null + && acceptHeader.contains(SAMLConstants.PAOS_HTTP_ACCEPT_HEADER) + && ("ver='" + org.opensaml.common.xml.SAMLConstants.PAOS_NS + "';'" + + org.opensaml.common.xml.SAMLConstants.SAML20ECP_NS + "'").equals( + request.getHeader(SAMLConstants.PAOS_HTTP_HEADER)); + } + /** * Method is supposed to populate preferences used to construct the SAML message. Method can be overridden to provide * logic appropriate for given application. In case defaultOptions object was set it will be used as basis for construction @@ -157,12 +192,13 @@ public void commence(HttpServletRequest request, HttpServletResponse response, A * * @param request request * @param response response + * @param context containing local entity * @param exception exception causing invocation of this entry point (can be null) * @return populated webSSOprofile * @throws MetadataProviderException in case metadata loading fails * @throws ServletException in case any other error occurs */ - protected WebSSOProfileOptions getProfileOptions(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws MetadataProviderException, ServletException { + protected WebSSOProfileOptions getProfileOptions(HttpServletRequest request, HttpServletResponse response, SAMLMessageContext context, AuthenticationException exception) throws MetadataProviderException, ServletException { WebSSOProfileOptions ssoProfileOptions; if (defaultOptions != null) { @@ -270,6 +306,14 @@ public void setWebSSOprofile(WebSSOProfile webSSOprofile) { this.webSSOprofile = webSSOprofile; } + public WebSSOProfile getWebSSOprofileECP() { + return webSSOprofileECP; + } + + public void setWebSSOprofileECP(WebSSOProfile webSSOprofileECP) { + this.webSSOprofileECP = webSSOprofileECP; + } + /** * Logger for SAML events, cannot be null, must be set. * @@ -317,4 +361,4 @@ public void afterPropertiesSet() throws ServletException { Assert.notNull(contextProvider, "Context provider must be set"); } -} \ No newline at end of file +} diff --git a/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/metadata/MetadataGenerator.java b/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/metadata/MetadataGenerator.java index bc66e22..d8747da 100644 --- a/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/metadata/MetadataGenerator.java +++ b/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/metadata/MetadataGenerator.java @@ -67,8 +67,6 @@ public class MetadataGenerator implements ApplicationContextAware { private boolean wantAssertionSigned = true; private boolean signMetadata = true; - - private String signingKey = null; private String encryptionKey = null; @@ -82,6 +80,7 @@ public class MetadataGenerator implements ApplicationContextAware { NameIDType.X509_SUBJECT); private static final Collection defaultBindings = Arrays.asList(SAMLConstants.SAML2_POST_BINDING_URI, + SAMLConstants.SAML2_PAOS_BINDING_URI, SAMLConstants.SAML2_ARTIFACT_BINDING_URI, SAMLConstants.SAML2_REDIRECT_BINDING_URI, SAMLConstants.SAML2_SOAP11_BINDING_URI); @@ -195,6 +194,11 @@ protected SPSSODescriptor buildSPSSODescriptor(String entityBaseURL, String enti index++; isDefault = false; } + if (includedBindings.contains(SAMLConstants.SAML2_PAOS_BINDING_URI)) { + spDescriptor.getAssertionConsumerServices().add(getAssertionConsumerService(entityBaseURL, entityAlias, isDefault, index, SAMLConstants.SAML2_PAOS_BINDING_URI)); + index++; + isDefault = false; + } if (includedBindings.contains(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) { spDescriptor.getSingleLogoutServices().add(getSingleLogoutService(entityBaseURL, entityAlias, SAMLConstants.SAML2_REDIRECT_BINDING_URI)); } diff --git a/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/processor/HTTPPAOS11Binding.java b/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/processor/HTTPPAOS11Binding.java new file mode 100755 index 0000000..b01c398 --- /dev/null +++ b/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/processor/HTTPPAOS11Binding.java @@ -0,0 +1,52 @@ +/* + * Copyright 2010 Jonathan Tellier + * + * 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.springframework.security.saml.processor; + +import javax.servlet.http.HttpServletRequest; + +import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.liberty.binding.decoding.HTTPPAOS11Decoder; +import org.opensaml.liberty.binding.encoding.HTTPPAOS11Encoder; +import org.opensaml.ws.transport.InTransport; +import org.opensaml.ws.transport.http.HttpServletRequestAdapter; +import org.opensaml.xml.parse.ParserPool; +import org.springframework.security.saml.processor.HTTPSOAP11Binding; + +public class HTTPPAOS11Binding extends HTTPSOAP11Binding { + + public HTTPPAOS11Binding(ParserPool parserPool) { + super(new HTTPPAOS11Decoder(parserPool), new HTTPPAOS11Encoder()); + } + + @Override + public boolean supports(InTransport transport) { + if (transport instanceof HttpServletRequestAdapter) { + HttpServletRequestAdapter t = (HttpServletRequestAdapter) transport; + HttpServletRequest request = t.getWrappedRequest(); + return "POST".equalsIgnoreCase(t.getHTTPMethod()) + && request.getContentType().startsWith( + org.springframework.security.saml.SAMLConstants.PAOS_HTTP_ACCEPT_HEADER); + } else { + return false; + } + } + + @Override + public String getCommunicationProfileId() { + return SAMLConstants.SAML2_PAOS_BINDING_URI; + } + +} diff --git a/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/websso/AbstractProfileBase.java b/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/websso/AbstractProfileBase.java index 85c9d74..8a4d549 100644 --- a/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/websso/AbstractProfileBase.java +++ b/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/websso/AbstractProfileBase.java @@ -29,6 +29,7 @@ import org.opensaml.saml2.metadata.provider.MetadataProviderException; import org.opensaml.security.MetadataCriteria; import org.opensaml.security.SAMLSignatureProfileValidator; +import org.opensaml.ws.message.encoder.MessageEncodingException; import org.opensaml.xml.XMLObjectBuilderFactory; import org.opensaml.xml.security.CriteriaSet; import org.opensaml.xml.security.credential.UsageType; @@ -144,6 +145,20 @@ protected SPSSODescriptor getSPDescriptor(String spId) throws MetadataProviderEx return spDescriptor; } + /** + * Method calls the processor and sends the message containted in the context. Subclasses can provide additional + * processing before the message delivery. + * + * @param context context + * @param sign whether the message should be signed + * @throws MetadataProviderException metadata error + * @throws SAMLException SAML encoding error + * @throws org.opensaml.ws.message.encoder.MessageEncodingException message encoding error + */ + protected void sendMessage(SAMLMessageContext context, boolean sign) throws MetadataProviderException, SAMLException, MessageEncodingException { + processor.sendMessage(context, sign); + } + protected Status getStatus(String code, String statusMessage) { SAMLObjectBuilder codeBuilder = (SAMLObjectBuilder) builderFactory.getBuilder(StatusCode.DEFAULT_ELEMENT_NAME); StatusCode statusCode = codeBuilder.buildObject(); @@ -170,11 +185,17 @@ protected Status getStatus(String code, String statusMessage) { * @param service service to use as destination for the request */ protected void buildCommonAttributes(RequestAbstractType request, Endpoint service) { + request.setID(generateID()); request.setIssuer(getIssuer()); request.setVersion(SAMLVersion.VERSION_20); request.setIssueInstant(new DateTime()); - request.setDestination(service.getLocation()); + + if (service != null) { + // Service is now known when we do not know which IDP will be used + request.setDestination(service.getLocation()); + } + } protected Issuer getIssuer() { diff --git a/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/websso/SingleLogoutProfileImpl.java b/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/websso/SingleLogoutProfileImpl.java index 4a50c28..6507305 100644 --- a/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/websso/SingleLogoutProfileImpl.java +++ b/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/websso/SingleLogoutProfileImpl.java @@ -77,7 +77,7 @@ public void sendLogoutRequest(SAMLMessageContext context, SAMLCredential credent context.setPeerExtendedMetadata(idpExtendedMetadata); boolean signMessage = context.getPeerExtendedMetadata().isRequireLogoutRequestSigned(); - processor.sendMessage(context, signMessage); + sendMessage(context, signMessage); messageStorage.storeMessage(logoutRequest.getID(), logoutRequest); } @@ -270,7 +270,7 @@ protected void sendLogoutResponse(Status status, SAMLMessageContext context) thr context.setPeerEntityRoleMetadata(idpDescriptor); boolean signMessage = context.getPeerExtendedMetadata().isRequireLogoutResponseSigned(); - processor.sendMessage(context, signMessage); + sendMessage(context, signMessage); } diff --git a/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/websso/WebSSOProfileECPImpl.java b/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/websso/WebSSOProfileECPImpl.java new file mode 100755 index 0000000..8d35f4a --- /dev/null +++ b/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/websso/WebSSOProfileECPImpl.java @@ -0,0 +1,146 @@ +/* + * Copyright 2011 Jonathan Tellier, Vladimir Schaefer + * + * 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.springframework.security.saml.websso; + +import org.opensaml.common.SAMLException; +import org.opensaml.common.SAMLObjectBuilder; +import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.saml2.core.AuthnRequest; +import org.opensaml.saml2.ecp.RelayState; +import org.opensaml.saml2.ecp.Request; +import org.opensaml.saml2.metadata.AssertionConsumerService; +import org.opensaml.saml2.metadata.SPSSODescriptor; +import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.opensaml.ws.message.encoder.MessageEncodingException; +import org.opensaml.ws.soap.common.SOAPObjectBuilder; +import org.opensaml.ws.soap.soap11.Envelope; +import org.opensaml.ws.soap.util.SOAPHelper; +import org.springframework.security.saml.context.SAMLMessageContext; +import org.springframework.security.saml.metadata.MetadataManager; +import org.springframework.security.saml.processor.SAMLProcessor; +import org.springframework.security.saml.storage.SAMLMessageStorage; +import org.springframework.security.saml.util.SAMLUtil; + +import java.util.Set; + +/** + * Class implementing the SAML ECP Profile and offers capabilities for SP initialized SSO and + * process Response coming from IDP or IDP initialized SSO. PAOS Binding is supported + * + * @author Jonathan Tellier, Vladimir Schaefer + */ +public class WebSSOProfileECPImpl extends WebSSOProfileImpl { + + public WebSSOProfileECPImpl() { + } + + public WebSSOProfileECPImpl(SAMLProcessor processor, MetadataManager manager) { + super(processor, manager); + } + + @Override + public void sendAuthenticationRequest(SAMLMessageContext context, WebSSOProfileOptions options, SAMLMessageStorage messageStorage) + throws SAMLException, MetadataProviderException, MessageEncodingException { + + SPSSODescriptor spDescriptor = getSPDescriptor(metadata.getHostedSPName()); + AssertionConsumerService assertionConsumer = SAMLUtil.getAssertionConsumerForBinding(spDescriptor, SAMLConstants.SAML2_PAOS_BINDING_URI); + + SOAPHelper.addHeaderBlock(context, getPAOSRequest(assertionConsumer)); + SOAPHelper.addHeaderBlock(context, getECPRequest(options)); + + if (context.getRelayState() != null) { + SOAPHelper.addHeaderBlock(context, getRelayState(context.getRelayState())); + } + + // The last parameter refers to the IdP that should receive the message. However, + // in ECP, we don't know in advance which IdP will be contacted. + AuthnRequest authRequest = getAuthnRequest(options, assertionConsumer, null); + + context.setCommunicationProfileId(SAMLConstants.SAML2_PAOS_BINDING_URI); + context.setOutboundMessage(getEnvelope()); + context.setOutboundSAMLMessage(authRequest); + + sendMessage(context, spDescriptor.isAuthnRequestsSigned()); + messageStorage.storeMessage(authRequest.getID(), authRequest); + + } + + protected org.opensaml.liberty.paos.Request getPAOSRequest(AssertionConsumerService assertionConsumer) { + + SAMLObjectBuilder paosRequestBuilder = (SAMLObjectBuilder) builderFactory.getBuilder(org.opensaml.liberty.paos.Request.DEFAULT_ELEMENT_NAME); + org.opensaml.liberty.paos.Request paosRequest = paosRequestBuilder.buildObject(); + + paosRequest.setSOAP11Actor(Request.SOAP11_ACTOR_NEXT); + paosRequest.setSOAP11MustUnderstand(true); + paosRequest.setResponseConsumerURL(assertionConsumer.getLocation()); + paosRequest.setService(SAMLConstants.SAML20ECP_NS); + + return paosRequest; + + } + + protected Request getECPRequest(WebSSOProfileOptions options) { + + SAMLObjectBuilder ecpRequestBuilder = (SAMLObjectBuilder) builderFactory.getBuilder(Request.DEFAULT_ELEMENT_NAME); + Request ecpRequest = ecpRequestBuilder.buildObject(); + + ecpRequest.setSOAP11Actor(Request.SOAP11_ACTOR_NEXT); + ecpRequest.setSOAP11MustUnderstand(true); + + ecpRequest.setPassive(options.getPassive()); + ecpRequest.setProviderName(options.getProviderName()); + ecpRequest.setIssuer(getIssuer()); + + Set idpEntityNames = options.getAllowedIDPs(); + if (options.isIncludeScoping() && idpEntityNames != null) { + ecpRequest.setIDPList(buildIDPList(idpEntityNames, null)); + } + + return ecpRequest; + + } + + protected Envelope getEnvelope() { + + SOAPObjectBuilder envelopeBuilder = (SOAPObjectBuilder) builderFactory.getBuilder(Envelope.DEFAULT_ELEMENT_NAME); + return envelopeBuilder.buildObject(); + + } + + /** + * Method creates a relayState element usable with the ECP profile. + * @param relayStateValue value to include, mustn't be null + * @return relay state object + */ + protected RelayState getRelayState(String relayStateValue) { + + if (relayStateValue == null) { + throw new IllegalArgumentException("RelayStateValue can't be null"); + } + if (relayStateValue.length() > 80) { + throw new IllegalArgumentException("Relay state can't exceed size 80 when using ECP profile"); + } + + SAMLObjectBuilder relayStateBuilder = (SAMLObjectBuilder) builderFactory.getBuilder(RelayState.DEFAULT_ELEMENT_NAME); + RelayState relayState = relayStateBuilder.buildObject(); + relayState.setSOAP11Actor(RelayState.SOAP11_ACTOR_NEXT); + relayState.setSOAP11MustUnderstand(true); + relayState.setValue(relayStateValue); + return relayState; + + } + +} \ No newline at end of file diff --git a/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/websso/WebSSOProfileImpl.java b/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/websso/WebSSOProfileImpl.java index 2095a71..fd534e3 100644 --- a/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/websso/WebSSOProfileImpl.java +++ b/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/websso/WebSSOProfileImpl.java @@ -32,6 +32,7 @@ import org.springframework.security.saml.util.SAMLUtil; import java.util.Collection; +import java.util.Set; /** * Class implements WebSSO profile and offers capabilities for SP initialized SSO and @@ -42,8 +43,6 @@ */ public class WebSSOProfileImpl extends AbstractProfileBase implements WebSSOProfile { - private static final int DEFAULT_PROXY_COUNT = 2; - public WebSSOProfileImpl() { } @@ -63,25 +62,27 @@ public WebSSOProfileImpl(SAMLProcessor processor, MetadataManager manager) { */ public void sendAuthenticationRequest(SAMLMessageContext context, WebSSOProfileOptions options, SAMLMessageStorage messageStorage) throws SAMLException, MetadataProviderException, MessageEncodingException { + // Verify we deal with a local SP + if (!SPSSODescriptor.DEFAULT_ELEMENT_NAME.equals(context.getLocalEntityRole())) { + throw new SAMLException("WebSSO can only be initialized for local SP, but localEntityRole is: " + context.getLocalEntityRole()); + } + // Initialize IDP based on options or use default String idpId = options.getIdp(); if (idpId == null) { idpId = metadata.getDefaultIDP(); } - // Verify we deal with a local SP - if (!SPSSODescriptor.DEFAULT_ELEMENT_NAME.equals(context.getLocalEntityRole())) { - throw new SAMLException("WebSSO can only be initialized for local SP, but localEntityRole is: " + context.getLocalEntityRole()); - } - + // Load the entities + SPSSODescriptor spDescriptor = (SPSSODescriptor) context.getLocalEntityRoleMetadata(); IDPSSODescriptor idpssoDescriptor = getIDPDescriptor(idpId); ExtendedMetadata idpExtendedMetadata = metadata.getExtendedMetadata(idpId); - SPSSODescriptor spDescriptor = (SPSSODescriptor) context.getLocalEntityRoleMetadata(); - String binding = SAMLUtil.getLoginBinding(options, idpssoDescriptor, spDescriptor); - AssertionConsumerService assertionConsumerForBinding = SAMLUtil.getAssertionConsumerForBinding(spDescriptor, binding); SingleSignOnService bindingService = SAMLUtil.getSSOServiceForBinding(idpssoDescriptor, binding); - AuthnRequest authRequest = getAuthnRequest(options, idpId, assertionConsumerForBinding, bindingService); + boolean sign = spDescriptor.isAuthnRequestsSigned() || idpssoDescriptor.getWantAuthnRequestsSigned(); + + AssertionConsumerService assertionConsumerForBinding = SAMLUtil.getAssertionConsumerForBinding(spDescriptor, binding); + AuthnRequest authRequest = getAuthnRequest(options, assertionConsumerForBinding, bindingService); // TODO optionally implement support for conditions, subject @@ -93,7 +94,7 @@ public void sendAuthenticationRequest(SAMLMessageContext context, WebSSOProfileO context.setPeerEntityRoleMetadata(idpssoDescriptor); context.setPeerExtendedMetadata(idpExtendedMetadata); - processor.sendMessage(context, spDescriptor.isAuthnRequestsSigned() || idpssoDescriptor.getWantAuthnRequestsSigned()); + sendMessage(context, sign); messageStorage.storeMessage(authRequest.getID(), authRequest); } @@ -103,14 +104,16 @@ public void sendAuthenticationRequest(SAMLMessageContext context, WebSSOProfileO * idpEntityDescriptor, with an expected response to the assertionConsumer address. * * @param options preferences of message creation - * @param idpEntityId entity ID of the IDP * @param assertionConsumer assertion consumer where the IDP should respond * @param bindingService service used to deliver the request * @return authnRequest ready to be sent to IDP * @throws SAMLException error creating the message * @throws MetadataProviderException error retreiving metadata */ - protected AuthnRequest getAuthnRequest(WebSSOProfileOptions options, String idpEntityId, AssertionConsumerService assertionConsumer, SingleSignOnService bindingService) throws SAMLException, MetadataProviderException { + protected AuthnRequest getAuthnRequest(WebSSOProfileOptions options, + AssertionConsumerService assertionConsumer, + SingleSignOnService bindingService) throws SAMLException, + MetadataProviderException { SAMLObjectBuilder builder = (SAMLObjectBuilder) builderFactory.getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME); AuthnRequest request = builder.buildObject(); @@ -118,19 +121,20 @@ protected AuthnRequest getAuthnRequest(WebSSOProfileOptions options, String idpE request.setForceAuthn(options.getForceAuthN()); buildCommonAttributes(request, bindingService); + + buildScoping(request, bindingService, options); builNameIDPolicy(request, options); buildAuthnContext(request, options); - buildScoping(request, idpEntityId, bindingService, options); buildReturnAddress(request, assertionConsumer); - return request; + return request; } /** * Fills the request with required AuthNContext according to selected options. * - * @param request request to fill - * @param options options driving generation of the element + * @param request request to fill + * @param options options driving generation of the element */ protected void builNameIDPolicy(AuthnRequest request, WebSSOProfileOptions options) { @@ -139,6 +143,8 @@ protected void builNameIDPolicy(AuthnRequest request, WebSSOProfileOptions optio NameIDPolicy nameIDPolicy = builder.buildObject(); nameIDPolicy.setFormat(options.getNameID()); nameIDPolicy.setAllowCreate(options.isAllowCreate()); + + // TODO The SPNameQualifier seems invalid when interacting with a Shibboleth IdP nameIDPolicy.setSPNameQualifier(getSPNameQualifier()); request.setNameIDPolicy(nameIDPolicy); } @@ -146,14 +152,14 @@ protected void builNameIDPolicy(AuthnRequest request, WebSSOProfileOptions optio } protected String getSPNameQualifier() { - return metadata.getHostedSPName(); + return metadata.getHostedSPName(); // TODO Fix } /** * Fills the request with required AuthNContext according to selected options. * - * @param request request to fill - * @param options options driving generation of the element + * @param request request to fill + * @param options options driving generation of the element */ protected void buildAuthnContext(AuthnRequest request, WebSSOProfileOptions options) { @@ -165,7 +171,7 @@ protected void buildAuthnContext(AuthnRequest request, WebSSOProfileOptions opti authnContext.setComparison(options.getAuthnContextComparison()); for (String context : contexts) { - + SAMLObjectBuilder contextRefBuilder = (SAMLObjectBuilder) builderFactory.getBuilder(AuthnContextClassRef.DEFAULT_ELEMENT_NAME); AuthnContextClassRef authnContextClassRef = contextRefBuilder.buildObject(); authnContextClassRef.setAuthnContextClassRef(context); @@ -196,36 +202,59 @@ private void buildReturnAddress(AuthnRequest request, AssertionConsumerService s /** * Fills the request with information about scoping, including IDP in the scope IDP List. * - * @param request request to fill - * @param idpEntityId id of the idp entity - * @param serviceURI destination to send the request to - * @param options options driving generation of the element + * @param request request to fill + * @param serviceURI destination to send the request to + * @param options options driving generation of the element, contains list of allowed IDPs */ - protected void buildScoping(AuthnRequest request, String idpEntityId, SingleSignOnService serviceURI, WebSSOProfileOptions options) { + protected void buildScoping(AuthnRequest request, SingleSignOnService serviceURI, WebSSOProfileOptions options) { if (options.isIncludeScoping()) { - SAMLObjectBuilder idpEntryBuilder = (SAMLObjectBuilder) builderFactory.getBuilder(IDPEntry.DEFAULT_ELEMENT_NAME); - IDPEntry idpEntry = idpEntryBuilder.buildObject(); - idpEntry.setProviderID(idpEntityId); - idpEntry.setLoc(serviceURI.getLocation()); - - SAMLObjectBuilder idpListBuilder = (SAMLObjectBuilder) builderFactory.getBuilder(IDPList.DEFAULT_ELEMENT_NAME); - IDPList idpList = idpListBuilder.buildObject(); - idpList.getIDPEntrys().add(idpEntry); - + Set idpEntityNames = options.getAllowedIDPs(); + IDPList idpList = buildIDPList(idpEntityNames, serviceURI); SAMLObjectBuilder scopingBuilder = (SAMLObjectBuilder) builderFactory.getBuilder(Scoping.DEFAULT_ELEMENT_NAME); Scoping scoping = scopingBuilder.buildObject(); scoping.setIDPList(idpList); + scoping.setProxyCount(options.getProxyCount()); + request.setScoping(scoping); - if (options.isAllowProxy()) { - scoping.setProxyCount(DEFAULT_PROXY_COUNT); - } + } - request.setScoping(scoping); + } + + /** + * Builds an IdP List out of the idpEntityNames + * + * @param idpEntityNames The IdPs Entity IDs to include in the IdP List, no list is created when null + * @param serviceURI The binding service for an IdP for a specific binding. Should be null + * if there is more than one IdP in the list or if the destination IdP is not known in + * advance. + * @return an IdP List or null when idpEntityNames is null + */ + protected IDPList buildIDPList(Set idpEntityNames, SingleSignOnService serviceURI) { + + if (idpEntityNames == null) { + return null; + } + + SAMLObjectBuilder idpEntryBuilder = (SAMLObjectBuilder) builderFactory.getBuilder(IDPEntry.DEFAULT_ELEMENT_NAME); + SAMLObjectBuilder idpListBuilder = (SAMLObjectBuilder) builderFactory.getBuilder(IDPList.DEFAULT_ELEMENT_NAME); + IDPList idpList = idpListBuilder.buildObject(); + for (String entityID : idpEntityNames) { + IDPEntry idpEntry = idpEntryBuilder.buildObject(); + idpEntry.setProviderID(entityID); + idpList.getIDPEntrys().add(idpEntry); + + // The service URI would be null if the SP does not know in advance + // to which IdP the request is sent to. + if (serviceURI != null) { + idpEntry.setLoc(serviceURI.getLocation()); + } } + return idpList; + } } diff --git a/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/websso/WebSSOProfileOptions.java b/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/websso/WebSSOProfileOptions.java index 6d857ba..5e40a90 100644 --- a/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/websso/WebSSOProfileOptions.java +++ b/spring-security-saml/saml2-core/src/main/java/org/springframework/security/saml/websso/WebSSOProfileOptions.java @@ -18,6 +18,7 @@ import java.io.Serializable; import java.util.Collection; +import java.util.Set; /** * JavaBean contains properties allowing customization of SAML request message sent to the IDP. @@ -28,15 +29,17 @@ public class WebSSOProfileOptions implements Serializable, Cloneable { private String idp; private String binding; + private Set allowedIDPs; + private String providerName; // Name ID policy private String nameID; private boolean allowCreate; private boolean passive = false; - private boolean forceAuthN = false; + private boolean forceAuthn = false; private boolean includeScoping = true; - private boolean allowProxy = true; + private Integer proxyCount = 2; private Collection authnContexts; private AuthnContextComparisonTypeEnumeration authnContextComparison = AuthnContextComparisonTypeEnumeration.EXACT; @@ -83,16 +86,22 @@ public boolean getPassive() { return passive; } + /** + * Sets whether the IdP should refrain from interacting with the user during the authentication process. Boolean + * values will be marshalled to either "true" or "false". + * + * @param passive true if passive authentication is allowed, false otherwise + */ public void setPassive(Boolean passive) { this.passive = passive; } public boolean getForceAuthN() { - return forceAuthN; + return forceAuthn; } public void setForceAuthN(Boolean forceAuthN) { - this.forceAuthN = forceAuthN; + this.forceAuthn = forceAuthN; } /** @@ -109,17 +118,23 @@ public void setIncludeScoping(boolean includeScoping) { } /** - * True is proxying should be allowed in requests sent to IDP as part of the generated Scoping element. - * Property includeScoping must be enabled for this value to take any effect. - * - * @return true if proxying is allowed + * @return null to skip proxyCount, 0 to disable proxying, >0 to allow proxying */ - public boolean isAllowProxy() { - return allowProxy; + public Integer getProxyCount() { + return proxyCount; } - public void setAllowProxy(boolean allowProxy) { - this.allowProxy = allowProxy; + /** + * Determines value to be used in the proxyCount attribute of the scope in the AuthnRequest. In case value is null + * the proxyCount attribute is omitted. Use zero to disable proxying or value >0 to specify how many hops are allowed. + *

+ * Property includeScoping must be enabled for this value to take any effect. + *

+ * + * @param proxyCount null to skip proxyCount in the AuthnRequest, 0 to disable proxying, >0 to allow proxying + */ + public void setProxyCount(Integer proxyCount) { + this.proxyCount = proxyCount; } public Collection getAuthnContexts() { @@ -144,10 +159,20 @@ public WebSSOProfileOptions clone() { } } + /** + * NameID to used or null to omit NameIDPolicy from request. + * + * @return name ID + */ public String getNameID() { return nameID; } + /** + * When set determines which NameIDPolicy will be requested as part of the AuthnRequest sent to the IDP. + * + * @param nameID name ID + */ public void setNameID(String nameID) { this.nameID = nameID; } @@ -177,5 +202,40 @@ public void setAuthnContextComparison(AuthnContextComparisonTypeEnumeration auth this.authnContextComparison = authnContextComparison; } } - + + public Set getAllowedIDPs() { + return allowedIDPs; + } + + /** + * List of IDPs which are allowed to process the created AuthnRequest. IDP the request will be sent to is added + * automatically. In case value is null the allowedIDPs will not be included in the Scoping element. + *

+ * Property includeScoping must be enabled for this value to take any effect. + *

+ * + * @param allowedIDPs IDPs enabled to process the created authnRequest, null to skip the attribute from scoptin + */ + public void setAllowedIDPs(Set allowedIDPs) { + this.allowedIDPs = allowedIDPs; + } + + /** + * Human readable name of the local entity. + * + * @return entity name + */ + public String getProviderName() { + return providerName; + } + + /** + * Sets human readable name of the local entity used in ECP profile. + * + * @param providerName provider name + */ + public void setProviderName(String providerName) { + this.providerName = providerName; + } + } diff --git a/spring-security-saml/saml2-core/src/test/java/org/springframework/security/saml/SAMLEntryPointTest.java b/spring-security-saml/saml2-core/src/test/java/org/springframework/security/saml/SAMLEntryPointTest.java index 700ee39..95d2b26 100644 --- a/spring-security-saml/saml2-core/src/test/java/org/springframework/security/saml/SAMLEntryPointTest.java +++ b/spring-security-saml/saml2-core/src/test/java/org/springframework/security/saml/SAMLEntryPointTest.java @@ -127,6 +127,8 @@ public void testIDPSelection() throws Exception { entryPoint.setIdpSelectionPath("/selectIDP"); expect(request.getParameter(SAMLEntryPoint.LOGIN_PARAMETER)).andReturn("false"); expect(request.getRequestDispatcher("/selectIDP")).andReturn(dispatcher); + expect(request.getHeader("Accept")).andReturn( + "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); dispatcher.include(request, response); replay(dispatcher); @@ -144,9 +146,9 @@ public void testIDPSelection() throws Exception { @Test public void testInitialProfileOptions() throws Exception { - WebSSOProfileOptions ssoProfileOptions = entryPoint.getProfileOptions(request, response, null); + WebSSOProfileOptions ssoProfileOptions = entryPoint.getProfileOptions(request, response, null, null); assertEquals("http://localhost:8080/opensso", ssoProfileOptions.getIdp()); - assertTrue(ssoProfileOptions.isAllowProxy()); + assertEquals(new Integer(2), ssoProfileOptions.getProxyCount()); assertTrue(ssoProfileOptions.isIncludeScoping()); assertFalse(ssoProfileOptions.getForceAuthN()); assertFalse(ssoProfileOptions.getPassive()); @@ -167,7 +169,7 @@ public void testDefaultProfileOptions() throws Exception { WebSSOProfileOptions defaultOptions = new WebSSOProfileOptions(); defaultOptions.setIdp("ignoredValue"); - defaultOptions.setAllowProxy(false); + defaultOptions.setProxyCount(0); defaultOptions.setIncludeScoping(false); defaultOptions.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); @@ -175,9 +177,9 @@ public void testDefaultProfileOptions() throws Exception { entryPoint.setDefaultProfileOptions(defaultOptions); // Check that default values are used - WebSSOProfileOptions ssoProfileOptions = entryPoint.getProfileOptions(request, response, null); + WebSSOProfileOptions ssoProfileOptions = entryPoint.getProfileOptions(request, response, null, null); assertEquals("http://localhost:8080/opensso", ssoProfileOptions.getIdp()); - assertFalse(ssoProfileOptions.isAllowProxy()); + assertEquals(new Integer(0), ssoProfileOptions.getProxyCount()); assertFalse(ssoProfileOptions.isIncludeScoping()); assertFalse(ssoProfileOptions.getForceAuthN()); assertFalse(ssoProfileOptions.getPassive()); @@ -185,13 +187,13 @@ public void testDefaultProfileOptions() throws Exception { // Check that value can't be altered after being set defaultOptions.setIncludeScoping(true); - ssoProfileOptions = entryPoint.getProfileOptions(request, response, null); + ssoProfileOptions = entryPoint.getProfileOptions(request, response, null, null); assertEquals("http://localhost:8080/opensso", ssoProfileOptions.getIdp()); assertFalse(ssoProfileOptions.isIncludeScoping()); // Check that default values can be cleared entryPoint.setDefaultProfileOptions(null); - ssoProfileOptions = entryPoint.getProfileOptions(request, response, null); + ssoProfileOptions = entryPoint.getProfileOptions(request, response, null, null); assertEquals("http://localhost:8080/opensso", ssoProfileOptions.getIdp()); assertTrue(ssoProfileOptions.isIncludeScoping()); @@ -214,6 +216,8 @@ public void testInvalidIDP() throws Exception { expect(session.getAttribute("_springSamlStorageKey")).andReturn(null); session.setAttribute(eq("_springSamlStorageKey"), notNull()); expect(request.getParameter("idp")).andReturn("testIDP"); + expect(request.getHeader("Accept")).andReturn( + "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); replayMock(); entryPoint.commence(request, response, null); @@ -235,6 +239,8 @@ public void testCorrectIDP() throws Exception { expect(session.getAttribute("_springSamlStorageKey")).andReturn(null); session.setAttribute(eq("_springSamlStorageKey"), notNull()); expect(request.getParameter("idp")).andReturn("http://localhost:8080/opensso"); + expect(request.getHeader("Accept")).andReturn( + "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); ssoProfile.sendAuthenticationRequest((SAMLMessageContext) notNull(), (WebSSOProfileOptions) notNull(), (SAMLMessageStorage) notNull()); replayMock(); diff --git a/spring-security-saml/saml2-core/src/test/java/org/springframework/security/saml/websso/WebSSOProfileImplTest.java b/spring-security-saml/saml2-core/src/test/java/org/springframework/security/saml/websso/WebSSOProfileImplTest.java index 1f0efd4..25ad043 100644 --- a/spring-security-saml/saml2-core/src/test/java/org/springframework/security/saml/websso/WebSSOProfileImplTest.java +++ b/spring-security-saml/saml2-core/src/test/java/org/springframework/security/saml/websso/WebSSOProfileImplTest.java @@ -1,4 +1,4 @@ -/* Copyright 2009 Vladimir Sch�fer +/* Copyright 2009 Vladimir Schafer * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ import static org.junit.Assert.*; /** - * @author Vladimir Sch�fer + * @author Vladimir Schafer */ public class WebSSOProfileImplTest extends SAMLTestBase { @@ -294,7 +294,7 @@ public void testForce() throws Exception { */ @Test public void testDisallowProxy() throws Exception { - options.setAllowProxy(false); + options.setProxyCount(null); storage.storeMessage((String) notNull(), (XMLObject) notNull()); replyMock(); profile.sendAuthenticationRequest(samlContext, options, storage); diff --git a/spring-security-saml/saml2-sample/pom.xml b/spring-security-saml/saml2-sample/pom.xml index 7e33105..eada829 100644 --- a/spring-security-saml/saml2-sample/pom.xml +++ b/spring-security-saml/saml2-sample/pom.xml @@ -55,6 +55,12 @@ 2.0 provided + + + javax.servlet + jstl + 1.2 + junit diff --git a/spring-security-saml/saml2-sample/src/main/resources/security/securityContext.xml b/spring-security-saml/saml2-sample/src/main/resources/security/securityContext.xml index 3e7c48a..97c7a05 100644 --- a/spring-security-saml/saml2-sample/src/main/resources/security/securityContext.xml +++ b/spring-security-saml/saml2-sample/src/main/resources/security/securityContext.xml @@ -28,7 +28,6 @@ class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler"> - @@ -117,7 +116,7 @@ - + http://idp.ssocircle.com/idp-meta.xml @@ -133,7 +132,7 @@ - + @@ -141,29 +140,28 @@ --> - + - - + - + - + @@ -179,19 +177,23 @@ + - + - - + + + + + - + @@ -223,9 +225,13 @@ + + + + - + diff --git a/spring-security-saml/saml2-sample/src/main/webapp/index.jsp b/spring-security-saml/saml2-sample/src/main/webapp/index.jsp index 358670d..313bcce 100644 --- a/spring-security-saml/saml2-sample/src/main/webapp/index.jsp +++ b/spring-security-saml/saml2-sample/src/main/webapp/index.jsp @@ -33,6 +33,27 @@

+

+ + + + + + + + + + + +
Principal's Attributes
+ +   + +
+

+