Java tutorial
/* 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. * 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 java.io.Serializable; import java.util.LinkedList; import java.util.List; import java.util.Random; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.xml.namespace.QName; import org.apache.commons.httpclient.HostConfiguration; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.URI; import org.apache.commons.httpclient.URIException; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.protocol.Protocol; import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; import org.joda.time.DateTime; import org.opensaml.Configuration; import org.opensaml.common.SAMLException; import org.opensaml.common.SAMLObject; import org.opensaml.common.SAMLObjectBuilder; import org.opensaml.common.SAMLRuntimeException; import org.opensaml.common.SAMLVersion; import org.opensaml.common.xml.SAMLConstants; import org.opensaml.saml2.core.Assertion; import org.opensaml.saml2.core.Attribute; import org.opensaml.saml2.core.AttributeQuery; import org.opensaml.saml2.core.AttributeStatement; import org.opensaml.saml2.core.AuthnRequest; import org.opensaml.saml2.core.EncryptedAssertion; import org.opensaml.saml2.core.EncryptedAttribute; import org.opensaml.saml2.core.Issuer; import org.opensaml.saml2.core.NameID; import org.opensaml.saml2.core.RequestAbstractType; import org.opensaml.saml2.core.Response; import org.opensaml.saml2.core.Subject; import org.opensaml.saml2.metadata.AttributeAuthorityDescriptor; import org.opensaml.saml2.metadata.AttributeService; import org.opensaml.saml2.metadata.Endpoint; import org.opensaml.saml2.metadata.IDPSSODescriptor; import org.opensaml.saml2.metadata.provider.MetadataProviderException; import org.opensaml.security.MetadataCriteria; import org.opensaml.ws.message.decoder.MessageDecodingException; import org.opensaml.ws.message.encoder.MessageEncodingException; import org.opensaml.ws.soap.client.http.TLSProtocolSocketFactory; import org.opensaml.ws.transport.http.HttpClientInTransport; import org.opensaml.ws.transport.http.HttpClientOutTransport; import org.opensaml.xml.XMLObject; import org.opensaml.xml.XMLObjectBuilder; import org.opensaml.xml.XMLObjectBuilderFactory; import org.opensaml.xml.encryption.DecryptionException; import org.opensaml.xml.schema.XSString; import org.opensaml.xml.security.CriteriaSet; import org.opensaml.xml.security.credential.UsageType; import org.opensaml.xml.security.criteria.EntityIDCriteria; import org.opensaml.xml.security.criteria.UsageCriteria; import org.opensaml.xml.validation.ValidationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.saml.SAMLCredential; import org.springframework.security.saml.context.SAMLContextProvider; import org.springframework.security.saml.context.SAMLMessageContext; import org.springframework.security.saml.metadata.ExtendedMetadata; import org.springframework.security.saml.processor.SAMLProcessor; import org.springframework.security.saml.storage.SAMLMessageStorage; import org.springframework.security.saml.trust.X509KeyManager; import org.springframework.security.saml.trust.X509TrustManager; import org.springframework.security.saml.util.SAMLUtil; import org.springframework.util.Assert; /** * @author Amish Gandhi */ public class AttributeQueryImpl { /** * Class logger. */ protected final static Logger log = LoggerFactory.getLogger(AttributeQueryImpl.class); XMLObjectBuilderFactory builderFactory; HttpClient httpClient; SAMLProcessor processor; protected SAMLContextProvider attrAuthContextProvider; SAMLMessageContext context; private AttributeQueryConsumer attributeQueryConsumer; /** * Sets entity responsible for populating local entity context data. * * @param contextProvider provider implementation */ @Autowired public void setAttrAuthContextProvider(SAMLContextProvider attrAuthContextProvider) { Assert.notNull(attrAuthContextProvider, "Context provider can't be null"); this.attrAuthContextProvider = attrAuthContextProvider; } @Autowired public void setAttributeQueryConsumer(AttributeQueryConsumer attributeQueryConsumer) { Assert.notNull(attributeQueryConsumer, "ssoConsumer can't be null"); this.attributeQueryConsumer = attributeQueryConsumer; } public AttributeQueryImpl(HttpClient httpClient, SAMLProcessor processor) { builderFactory = Configuration.getBuilderFactory(); this.httpClient = httpClient; //processor = new SAMLProcessorImpl(new HTTPSOAP11Binding(new StaticBasicParserPool())); this.processor = processor; } public void testAttributeQuery(HttpServletRequest request, HttpServletResponse response) { try { SAMLMessageContext context = attrAuthContextProvider.getLocalAndPeerEntity(request, response); createAttributeContext(context); getAttributeResponse(context); } catch (Exception e) { e.printStackTrace(); } } /** * Uses HTTPClient to send and retrieve ArtifactMessages. * * @param endpointURI URI incoming artifactMessage is addressed to * @param context context with filled communicationProfileId, outboundMessage, outboundSAMLMessage, peerEntityEndpoint, peerEntityId, peerEntityMetadata, peerEntityRole, peerEntityRoleMetadata * @throws SAMLException error processing artifact messages * @throws MessageEncodingException error sending artifactRequest * @throws MessageDecodingException error retrieving artifactResponse * @throws MetadataProviderException error resolving metadata * @throws org.opensaml.xml.security.SecurityException * invalid message signature */ protected void getAttributeResponse(SAMLMessageContext context) throws SAMLException, MessageEncodingException, MessageDecodingException, MetadataProviderException, org.opensaml.xml.security.SecurityException { PostMethod postMethod = null; try { URI uri = new URI(context.getPeerEntityEndpoint().getLocation(), true, "UTF-8"); postMethod = new PostMethod(); postMethod.setPath(uri.getPath()); HostConfiguration hc = getHostConfiguration(uri, context); HttpClientOutTransport clientOutTransport = new HttpClientOutTransport(postMethod); //HttpClientInTransport clientInTransport = new HttpClientInTransport(postMethod, "http://10.200.49.21:8080/spring-security-saml2-sample/saml/SSO/alias/defaultAlias"); HttpClientInTransport clientInTransport = new HttpClientInTransport(postMethod, ""); context.setInboundMessageTransport(clientInTransport); context.setOutboundMessageTransport(clientOutTransport); // Send artifact retrieve message boolean signMessage = true; processor.sendMessage(context, signMessage, SAMLConstants.SAML2_SOAP11_BINDING_URI); log.debug("Sending Attribute message to {}", uri); int responseCode = httpClient.executeMethod(hc, postMethod); if (responseCode != 200) { String responseBody = postMethod.getResponseBodyAsString(); throw new MessageDecodingException( "Problem communicating with Attribute Query service, received response " + responseCode + ", body " + responseBody); } // Decode artifact response message. processor.retrieveMessage(context, SAMLConstants.SAML2_SOAP11_BINDING_URI); List<Attribute> attributes = attributeQueryConsumer.processAttributeQueryResponse(context); } catch (Exception e) { e.printStackTrace(); throw new MessageDecodingException("Error when sending request to artifact resolution service.", e); } finally { if (postMethod != null) { postMethod.releaseConnection(); } } } /** * Initializes SSO by creating AuthnRequest assertion and sending it to the IDP using the default binding. * Default IDP is used to send the request. * * * @param options values specified by caller to customize format of sent request * @throws SAMLException error initializing SSO * @throws SAMLRuntimeException in case context doesn't contain required entities or contains invalid data * @throws MetadataProviderException error retrieving needed metadata * @throws MessageEncodingException error forming SAML message */ public void createAttributeContext(SAMLMessageContext context) throws SAMLException, MetadataProviderException, MessageEncodingException { // Verify we deal with a local SP if (!AttributeAuthorityDescriptor.DEFAULT_ELEMENT_NAME.equals(context.getLocalEntityRole())) { throw new SAMLException("AttributeQuery can only be initialized for local SP, but localEntityRole is: " + context.getLocalEntityRole()); } // Load the entities from the context AttributeAuthorityDescriptor idpattrDescriptor = (AttributeAuthorityDescriptor) context .getPeerEntityRoleMetadata(); ExtendedMetadata idpExtendedMetadata = context.getPeerExtendedMetadata(); if (idpattrDescriptor == null || idpExtendedMetadata == null) { throw new SAMLException( "AttributeAuthorityDescriptor, AttributeAuthorityDescriptor or IDPExtendedMetadata are not present in the SAMLContext"); } AttributeService attrService = getAttributeService(idpattrDescriptor); AttributeQuery query = getAttributeRequest(context); context.setCommunicationProfileId("urn:oasis:names:tc:SAML:2.0:attrname-format:basic"); context.setOutboundMessage(query); context.setOutboundSAMLMessage(query); context.setPeerEntityEndpoint(attrService); context.setPeerEntityId(idpattrDescriptor.getID()); context.setPeerEntityRoleMetadata(idpattrDescriptor); context.setPeerExtendedMetadata(idpExtendedMetadata); SAMLMessageStorage messageStorage = context.getMessageStorage(); if (messageStorage != null) { messageStorage.storeMessage(query.getID(), query); } } /** * Method determines SingleSignOn service (and thus binding) to be used to deliver AuthnRequest to the IDP. * When binding is specified in the WebSSOProfileOptions it is honored. Otherwise first suitable binding is used. * * @param options user supplied preferences, binding attribute is used * @param idpattrDescriptor idp * @param spDescriptor sp * @return service to send message to * @throws MetadataProviderException in case binding from the options is invalid or not found or when no default service can be found */ protected AttributeService getAttributeService(AttributeAuthorityDescriptor idpattrDescriptor) throws MetadataProviderException { // Find the endpoint List<AttributeService> services = idpattrDescriptor.getAttributeServices(); //TODO Return the binding based on option selected for (AttributeService service : services) { // Use as a default return service; } return null; } protected AttributeQuery getAttributeRequest(SAMLMessageContext context) throws SAMLException, MetadataProviderException { SAMLObjectBuilder<AttributeQuery> builder = (SAMLObjectBuilder<AttributeQuery>) builderFactory .getBuilder(AttributeQuery.DEFAULT_ELEMENT_NAME); AttributeQuery request = builder.buildObject(); SAMLObjectBuilder<Attribute> attributeBuilder = (SAMLObjectBuilder<Attribute>) builderFactory .getBuilder(Attribute.DEFAULT_ELEMENT_NAME); XMLObjectBuilder<XSString> xsstringBuilder = (XMLObjectBuilder<XSString>) builderFactory .getBuilder(XSString.TYPE_NAME); // XSString xsstring = xsstringBuilder // xsstring.se List<Attribute> attributes = request.getAttributes(); Attribute attribute = attributeBuilder.buildObject(); attribute.setFriendlyName("mail"); attribute.setName("urn:oid:0.9.2342.19200300.100.1.3"); attribute.setNameFormat("urn:oasis:names:tc:SAML:2.0:attrname-format:uri"); List<XMLObject> values = attribute.getAttributeValues(); QName qName = new QName("string", "http://www.w3.org/2001/XMLSchema"); XSString xsstring = xsstringBuilder.buildObject(qName); xsstring.setValue("ccm1001@atseng.com"); values.add(xsstring); //attributes.add(attribute); SAMLObjectBuilder<Subject> subjectBuilder = (SAMLObjectBuilder<Subject>) builderFactory .getBuilder(Subject.DEFAULT_ELEMENT_NAME); Subject subject = subjectBuilder.buildObject(); SAMLObjectBuilder<NameID> nameIDbuilder = (SAMLObjectBuilder<NameID>) builderFactory .getBuilder(NameID.DEFAULT_ELEMENT_NAME); NameID nameID = nameIDbuilder.buildObject(); nameID.setFormat("urn:oasis:names:tc:SAML:1.1:nameid-format:principalName"); nameID.setValue("ccmsso1"); subject.setNameID(nameID); request.setSubject(subject); request.setVersion(SAMLVersion.VERSION_20); buildCommonAttributes(context.getLocalEntityId(), request); return request; } /** * Fills the request with version, issue instants and destination data. * * @param localEntityId entityId of the local party acting as message issuer * @param request request to be filled * @param service service to use as destination for the request */ protected void buildCommonAttributes(String localEntityId, RequestAbstractType request) { request.setID(generateID()); request.setIssuer(getIssuer(localEntityId)); request.setVersion(SAMLVersion.VERSION_20); request.setIssueInstant(new DateTime()); /* if (service != null) { // Service is now known when we do not know which IDP will be used request.setDestination(service.getLocation()); }*/ } protected Issuer getIssuer(String localEntityId) { SAMLObjectBuilder<Issuer> issuerBuilder = (SAMLObjectBuilder<Issuer>) builderFactory .getBuilder(Issuer.DEFAULT_ELEMENT_NAME); Issuer issuer = issuerBuilder.buildObject(); issuer.setValue(localEntityId); return issuer; } /** * Generates random ID to be used as Request/Response ID. * * @return random ID */ protected String generateID() { Random r = new Random(); return 'a' + Long.toString(Math.abs(r.nextLong()), 20) + Long.toString(Math.abs(r.nextLong()), 20); } /** * SAML-Core 2218, Specifies that returned subject identifier should be returned in the namespace of the given SP. * * @return by default returns null */ protected String getSPNameQualifier() { return null; } /** * Method is expected to return binding used to transfer messages to this endpoint. For some profiles the * binding attribute in the metadata contains the profile name, method correctly parses the real binding * in these situations. * * @param endpoint endpoint * @return binding */ protected String getEndpointBinding(Endpoint endpoint) { return SAMLUtil.getBindingForEndpoint(endpoint); } /** * Method is expected to determine hostConfiguration used to send request to the server by back-channel. Configuration * should contain URI of the host and used protocol including all security settings. * <p/> * Default implementation uses either default http protocol for non-SSL requests or constructs a separate * TrustManager using trust engine specified in the SAMLMessageContext - based either on MetaIOP (certificates * obtained from Metadata and ExtendedMetadata are trusted) or PKIX (certificates from metadata and ExtendedMetadata * including specified trust anchors are trusted and verified using PKIX). * <p/> * Used trust engine can be customized as part of the SAMLContextProvider used to process this request. * <p/> * Default values for the HostConfiguration are cloned from the HTTPClient set in this instance, when there are * no defaults available a new object is created. * * @param uri uri the request should be sent to * @param context context including the peer address * @return host configuration * @throws MessageEncodingException in case peer URI can't be parsed */ protected HostConfiguration getHostConfiguration(URI uri, SAMLMessageContext context) throws MessageEncodingException { try { HostConfiguration hc = httpClient.getHostConfiguration(); if (hc != null) { // Clone configuration from the HTTP Client object hc = new HostConfiguration(hc); } else { // Create brand new configuration when there are no defaults hc = new HostConfiguration(); } if (uri.getScheme().equalsIgnoreCase("http")) { log.debug("Using HTTP configuration"); hc.setHost(uri); } else { log.debug("Using HTTPS configuration"); CriteriaSet criteriaSet = new CriteriaSet(); criteriaSet.add(new EntityIDCriteria(context.getPeerEntityId())); criteriaSet .add(new MetadataCriteria(IDPSSODescriptor.DEFAULT_ELEMENT_NAME, SAMLConstants.SAML20P_NS)); criteriaSet.add(new UsageCriteria(UsageType.UNSPECIFIED)); X509TrustManager trustManager = new X509TrustManager(criteriaSet, context.getLocalSSLTrustEngine()); X509KeyManager manager = new X509KeyManager(context.getLocalSSLCredential()); Protocol protocol = new Protocol("https", (ProtocolSocketFactory) new TLSProtocolSocketFactory(manager, trustManager), 443); hc.setHost(uri.getHost(), uri.getPort(), protocol); } return hc; } catch (URIException e) { throw new MessageEncodingException("Error parsing remote location URI", e); } } }