2 * The Shibboleth License, Version 1. Copyright (c) 2002 University Corporation for Advanced Internet Development, Inc.
3 * All rights reserved Redistribution and use in source and binary forms, with or without modification, are permitted
4 * provided that the following conditions are met: Redistributions of source code must retain the above copyright
5 * notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above
6 * copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials
7 * provided with the distribution, if any, must include the following acknowledgment: "This product includes software
8 * developed by the University Corporation for Advanced Internet Development <http://www.ucaid.edu> Internet2 Project.
9 * Alternately, this acknowledegement may appear in the software itself, if and wherever such third-party
10 * acknowledgments normally appear. Neither the name of Shibboleth nor the names of its contributors, nor Internet2, nor
11 * the University Corporation for Advanced Internet Development, Inc., nor UCAID may be used to endorse or promote
12 * products derived from this software without specific prior written permission. For written permission, please contact
13 * shibboleth@shibboleth.org Products derived from this software may not be called Shibboleth, Internet2, UCAID, or the
14 * University Corporation for Advanced Internet Development, nor may Shibboleth appear in their name, without prior
15 * written permission of the University Corporation for Advanced Internet Development. THIS SOFTWARE IS PROVIDED BY THE
16 * COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE
18 * DISCLAIMED AND THE ENTIRE RISK OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE. IN NO
19 * EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC.
20 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
23 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 package edu.internet2.middleware.shibboleth.idp.provider;
28 import java.io.IOException;
29 import java.net.URLEncoder;
30 import java.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.Collections;
33 import java.util.Date;
34 import java.util.Iterator;
35 import java.util.Vector;
37 import javax.servlet.RequestDispatcher;
38 import javax.servlet.ServletException;
39 import javax.servlet.http.HttpServletRequest;
40 import javax.servlet.http.HttpServletResponse;
41 import javax.xml.namespace.QName;
43 import org.apache.log4j.Logger;
44 import org.opensaml.SAMLAssertion;
45 import org.opensaml.SAMLAttribute;
46 import org.opensaml.SAMLAttributeStatement;
47 import org.opensaml.SAMLAudienceRestrictionCondition;
48 import org.opensaml.SAMLAuthenticationStatement;
49 import org.opensaml.SAMLAuthorityBinding;
50 import org.opensaml.SAMLBinding;
51 import org.opensaml.SAMLBrowserProfile;
52 import org.opensaml.SAMLCondition;
53 import org.opensaml.SAMLException;
54 import org.opensaml.SAMLNameIdentifier;
55 import org.opensaml.SAMLRequest;
56 import org.opensaml.SAMLResponse;
57 import org.opensaml.SAMLStatement;
58 import org.opensaml.SAMLSubject;
59 import org.opensaml.artifact.Artifact;
60 import org.w3c.dom.Document;
61 import org.w3c.dom.Element;
63 import sun.misc.BASE64Decoder;
65 import edu.internet2.middleware.shibboleth.aa.AAException;
66 import edu.internet2.middleware.shibboleth.common.AuthNPrincipal;
67 import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException;
68 import edu.internet2.middleware.shibboleth.common.RelyingParty;
69 import edu.internet2.middleware.shibboleth.common.ShibBrowserProfile;
70 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
71 import edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler;
72 import edu.internet2.middleware.shibboleth.idp.IdPProtocolSupport;
73 import edu.internet2.middleware.shibboleth.idp.InvalidClientDataException;
75 import edu.internet2.middleware.shibboleth.metadata.Endpoint;
76 import edu.internet2.middleware.shibboleth.metadata.EntityDescriptor;
77 import edu.internet2.middleware.shibboleth.metadata.SPSSODescriptor;
80 * @author Walter Hoehn
82 public class ShibbolethV1SSOHandler extends BaseHandler implements IdPProtocolHandler {
84 private static Logger log = Logger.getLogger(ShibbolethV1SSOHandler.class.getName());
87 * Required DOM-based constructor.
89 public ShibbolethV1SSOHandler(Element config) throws ShibbolethConfigurationException {
95 * @see edu.internet2.middleware.shibboleth.idp.IdPResponder.ProtocolHandler#processRequest(javax.servlet.http.HttpServletRequest,
96 * javax.servlet.http.HttpServletResponse)
98 public SAMLResponse processRequest(HttpServletRequest request, HttpServletResponse response,
99 SAMLRequest samlRequest, IdPProtocolSupport support) throws SAMLException, ServletException, IOException {
101 // TODO attribute push?
103 if (request == null) {
104 log.error("Protocol Handler received a SAML Request, but is unable to handle it.");
105 throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
108 // Set attributes that are needed by the jsp
109 request.setAttribute("shire", request.getParameter("shire"));
110 request.setAttribute("target", request.getParameter("target"));
113 // Ensure that we have the required data from the servlet container
114 IdPProtocolSupport.validateEngineData(request);
115 validateShibSpecificData(request);
117 // Get the authN info
118 String username = support.getIdPConfig().getAuthHeaderName().equalsIgnoreCase("REMOTE_USER") ? request
119 .getRemoteUser() : request.getHeader(support.getIdPConfig().getAuthHeaderName());
120 if ((username == null) || (username.equals(""))) { throw new InvalidClientDataException(
121 "Unable to authenticate remote user"); }
123 // Select the appropriate Relying Party configuration for the request
124 RelyingParty relyingParty = null;
125 String remoteProviderId = request.getParameter("providerId");
126 // If the target did not send a Provider Id, then assume it is a Shib
127 // 1.1 or older target
128 if (remoteProviderId == null) {
129 relyingParty = support.getServiceProviderMapper().getLegacyRelyingParty();
130 } else if (remoteProviderId.equals("")) {
131 throw new InvalidClientDataException("Invalid service provider id.");
133 log.debug("Remote provider has identified itself as: (" + remoteProviderId + ").");
134 relyingParty = support.getServiceProviderMapper().getRelyingParty(remoteProviderId);
137 // Grab the metadata for the provider
138 EntityDescriptor provider = support.lookup(relyingParty.getProviderId());
140 // Determine the acceptance URL
141 String acceptanceURL = request.getParameter("shire");
143 // Make sure that the selected relying party configuration is appropriate for this
145 if (!relyingParty.isLegacyProvider()) {
147 if (provider == null) {
148 log.info("No metadata found for provider: (" + relyingParty.getProviderId() + ").");
149 relyingParty = support.getServiceProviderMapper().getRelyingParty(null);
153 if (isValidAssertionConsumerURL(provider, acceptanceURL)) {
154 log.info("Supplied consumer URL validated for this provider.");
156 log.error("Assertion consumer service URL (" + acceptanceURL + ") is NOT valid for provider ("
157 + relyingParty.getProviderId() + ").");
158 throw new InvalidClientDataException("Invalid assertion consumer service URL.");
163 // Create SAML Name Identifier
164 SAMLNameIdentifier nameId;
166 nameId = support.getNameMapper().getNameIdentifierName(relyingParty.getHSNameFormatId(),
167 new AuthNPrincipal(username), relyingParty, relyingParty.getIdentityProvider());
168 } catch (NameIdentifierMappingException e) {
169 log.error("Error converting principal to SAML Name Identifier: " + e);
170 throw new SAMLException("Error converting principal to SAML Name Identifier.", e);
173 String authenticationMethod = request.getHeader("SAMLAuthenticationMethod");
174 if (authenticationMethod == null || authenticationMethod.equals("")) {
175 authenticationMethod = relyingParty.getDefaultAuthMethod().toString();
176 log.debug("User was authenticated via the default method for this relying party ("
177 + authenticationMethod + ").");
179 log.debug("User was authenticated via the method (" + authenticationMethod + ").");
182 // TODO Provide a mechanism for the authenticator to specify the auth time
184 SAMLSubject authNSubject = new SAMLSubject(nameId, null, null, null);
186 ArrayList assertions = new ArrayList();
188 // TODO push support cleanup???
191 // TODO error out if legacy and push
192 SAMLAttribute[] attrs;
194 attrs = support.getReleaseAttributes(new AuthNPrincipal(username), relyingParty.getProviderId(),
197 if (attrs != null && attrs.length > 0) {
198 // Reference requested subject
199 SAMLSubject attrSubject;
201 attrSubject = (SAMLSubject) authNSubject.clone();
204 ArrayList audiences = new ArrayList();
205 if (relyingParty.getProviderId() != null) {
206 audiences.add(relyingParty.getProviderId());
208 if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
209 audiences.add(relyingParty.getName());
211 SAMLCondition condition = new SAMLAudienceRestrictionCondition(audiences);
213 // Put all attributes into an assertion
214 SAMLStatement statement = new SAMLAttributeStatement(attrSubject, Arrays.asList(attrs));
216 // Set assertion expiration to longest attribute expiration
218 for (int i = 0; i < attrs.length; i++) {
219 if (max < attrs[i].getLifetime()) {
220 max = attrs[i].getLifetime();
223 Date now = new Date();
224 Date then = new Date(now.getTime() + (max * 1000)); // max is in seconds
226 SAMLAssertion attrAssertion = new SAMLAssertion(relyingParty.getIdentityProvider().getProviderId(),
227 now, then, Collections.singleton(condition), null, Collections.singleton(statement));
228 if (log.isDebugEnabled()) {
229 log.debug("Dumping generated Attribute Assertion:" + System.getProperty("line.separator")
230 + new String(new BASE64Decoder().decodeBuffer(new String(attrAssertion.toBase64(), "ASCII")), "UTF8"));
232 assertions.add(attrAssertion);
233 // TODO make sure signature adds covers this stuff
235 //TODO remove this message
236 log.debug("No Attrs!");
238 } catch (AAException e2) {
239 // TODO Auto-generated catch block
240 e2.printStackTrace();
241 } catch (CloneNotSupportedException e1) {
242 // TODO Auto-generated catch block
243 e1.printStackTrace();
247 // TODO do assertion signing for artifact stuff
249 // SAML Artifact profile
250 if (useArtifactProfile(provider, acceptanceURL)) {
251 log.debug("Responding with Artifact profile.");
253 // TODO woa! error if legacy
255 authNSubject.addConfirmationMethod(SAMLSubject.CONF_ARTIFACT);
257 assertions.add(generateAuthNAssertion(request, relyingParty, provider, nameId, authenticationMethod,
258 new Date(System.currentTimeMillis()), authNSubject));
260 // Create artifacts for each assertion
261 ArrayList artifacts = new ArrayList();
262 for (int i = 0; i < assertions.size(); i++) {
263 artifacts.add(support.getArtifactMapper().generateArtifact((SAMLAssertion) assertions.get(i),
267 // Assemble the query string
268 StringBuffer destination = new StringBuffer(acceptanceURL);
269 destination.append("?TARGET=");
271 destination.append(URLEncoder.encode(request.getParameter("target"), "UTF-8"));
273 Iterator iterator = artifacts.iterator();
274 StringBuffer artifactBuffer = new StringBuffer(); // Buffer for the transaction log
276 // Construct the artifact query parameter
277 while (iterator.hasNext()) {
278 Artifact artifact = (Artifact) iterator.next();
279 artifactBuffer.append("(" + artifact + ")");
280 destination.append("&SAMLart=");
281 destination.append(URLEncoder.encode(artifact.encode(), "UTF-8"));
284 log.debug("Redirecting to (" + destination.toString() + ").");
285 response.sendRedirect(destination.toString()); // Redirect to the artifact receiver
287 support.getTransactionLog().info(
288 "Assertion artifact(s) (" + artifactBuffer.toString() + ") issued to provider ("
289 + relyingParty.getIdentityProvider().getProviderId() + ") on behalf of principal ("
290 + username + "). Name Identifier: (" + nameId.getName()
291 + "). Name Identifier Format: (" + nameId.getFormat() + ").");
295 log.debug("Responding with POST profile.");
297 authNSubject.addConfirmationMethod(SAMLSubject.CONF_BEARER);
299 assertions.add(generateAuthNAssertion(request, relyingParty, provider, nameId, authenticationMethod,
300 new Date(System.currentTimeMillis()), authNSubject));
302 request.setAttribute("acceptanceURL", acceptanceURL);
303 request.setAttribute("target", request.getParameter("target"));
305 SAMLResponse samlResponse = new SAMLResponse(null, acceptanceURL, assertions, null);
306 IdPProtocolSupport.addSignatures(samlResponse, relyingParty, provider, true);
307 createPOSTForm(request, response, samlResponse.toBase64());
309 // Make transaction log entry
310 if (relyingParty.isLegacyProvider()) {
311 support.getTransactionLog().info(
312 "Authentication assertion issued to legacy provider (SHIRE: "
313 + request.getParameter("shire") + ") on behalf of principal (" + username
314 + ") for resource (" + request.getParameter("target") + "). Name Identifier: ("
315 + nameId.getName() + "). Name Identifier Format: (" + nameId.getFormat() + ").");
317 support.getTransactionLog().info(
318 "Authentication assertion issued to provider ("
319 + relyingParty.getIdentityProvider().getProviderId() + ") on behalf of principal ("
320 + username + "). Name Identifier: (" + nameId.getName()
321 + "). Name Identifier Format: (" + nameId.getFormat() + ").");
324 } catch (InvalidClientDataException e) {
325 throw new SAMLException(SAMLException.RESPONDER, e.getMessage());
330 private SAMLAssertion generateAuthNAssertion(HttpServletRequest request, RelyingParty relyingParty,
331 EntityDescriptor provider, SAMLNameIdentifier nameId, String authenticationMethod, Date authTime,
332 SAMLSubject subject) throws SAMLException, IOException {
334 Document doc = org.opensaml.XML.parserPool.newDocument();
336 // Determine the correct audiences
337 ArrayList audiences = new ArrayList();
338 if (relyingParty.getProviderId() != null) {
339 audiences.add(relyingParty.getProviderId());
341 if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
342 audiences.add(relyingParty.getName());
345 // Determine the correct issuer
346 String issuer = null;
347 if (relyingParty.isLegacyProvider()) {
349 log.debug("Service Provider is running Shibboleth <= 1.1. Using old style issuer.");
350 if (relyingParty.getIdentityProvider().getSigningCredential() == null
351 || relyingParty.getIdentityProvider().getSigningCredential().getX509Certificate() == null) { throw new SAMLException(
352 "Cannot serve legacy style assertions without an X509 certificate"); }
353 issuer = ShibBrowserProfile.getHostNameFromDN(relyingParty.getIdentityProvider().getSigningCredential()
354 .getX509Certificate().getSubjectX500Principal());
355 if (issuer == null || issuer.equals("")) { throw new SAMLException(
356 "Error parsing certificate DN while determining legacy issuer name."); }
359 issuer = relyingParty.getIdentityProvider().getProviderId();
362 // For compatibility with pre-1.2 shibboleth targets, include a pointer to the AA
363 ArrayList bindings = new ArrayList();
364 if (relyingParty.isLegacyProvider()) {
366 SAMLAuthorityBinding binding = new SAMLAuthorityBinding(SAMLBinding.SOAP, relyingParty.getAAUrl()
367 .toString(), new QName(org.opensaml.XML.SAMLP_NS, "AttributeQuery"));
368 bindings.add(binding);
371 // Create the assertion
372 Vector conditions = new Vector(1);
373 if (audiences != null && audiences.size() > 0) conditions.add(new SAMLAudienceRestrictionCondition(audiences));
375 SAMLStatement[] statements = {new SAMLAuthenticationStatement(subject, authenticationMethod, authTime, request
376 .getRemoteAddr(), null, bindings)};
378 SAMLAssertion assertion = new SAMLAssertion(issuer, new Date(System.currentTimeMillis()), new Date(System
379 .currentTimeMillis() + 300000), conditions, null, Arrays.asList(statements));
381 if (log.isDebugEnabled()) {
382 log.debug("Dumping generated AuthN Assertion:" + System.getProperty("line.separator")
383 + new String(new BASE64Decoder().decodeBuffer(new String(assertion.toBase64(), "ASCII")), "UTF8"));
390 * @see edu.internet2.middleware.shibboleth.idp.IdPResponder.ProtocolHandler#getHandlerName()
392 public String getHandlerName() {
394 return "Shibboleth v1.x SSO";
397 private void validateShibSpecificData(HttpServletRequest request) throws InvalidClientDataException {
399 if (request.getParameter("target") == null || request.getParameter("target").equals("")) { throw new InvalidClientDataException(
400 "Invalid data from Service Provider: no target URL received."); }
401 if ((request.getParameter("shire") == null) || (request.getParameter("shire").equals(""))) { throw new InvalidClientDataException(
402 "Invalid data from Service Provider: No acceptance URL received."); }
405 private static void createPOSTForm(HttpServletRequest req, HttpServletResponse res, byte[] buf) throws IOException,
408 // Hardcoded to ASCII to ensure Base64 encoding compatibility
409 req.setAttribute("assertion", new String(buf, "ASCII"));
411 if (log.isDebugEnabled()) {
413 log.debug("Dumping generated SAML Response:" + System.getProperty("line.separator")
414 + new String(new BASE64Decoder().decodeBuffer(new String(buf, "ASCII")), "UTF8"));
415 } catch (IOException e) {
416 log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
420 RequestDispatcher rd = req.getRequestDispatcher("/IdP.jsp");
421 rd.forward(req, res);
424 private static boolean useArtifactProfile(EntityDescriptor provider, String acceptanceURL) {
426 // Default to POST if we have no metadata
427 if (provider == null) { return false; }
429 // Default to POST if we have incomplete metadata
430 SPSSODescriptor sp = provider.getSPSSODescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
431 if (sp == null) { return false; }
433 // Look at the bindings.. prefer POST if we have multiple
434 Iterator endpoints = sp.getAssertionConsumerServiceManager().getEndpoints();
435 while (endpoints.hasNext()) {
436 Endpoint ep = (Endpoint) endpoints.next();
437 if (acceptanceURL.equals(ep.getLocation()) && SAMLBrowserProfile.PROFILE_POST_URI.equals(ep.getBinding())) { return false; }
438 if (acceptanceURL.equals(ep.getLocation())
439 && SAMLBrowserProfile.PROFILE_ARTIFACT_URI.equals(ep.getBinding())) { return true; }
442 // Default to POST if we have incomplete metadata
446 private static boolean isValidAssertionConsumerURL(EntityDescriptor provider, String shireURL)
447 throws InvalidClientDataException {
449 SPSSODescriptor sp = provider.getSPSSODescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
451 log.info("Inappropriate metadata for provider.");
455 Iterator endpoints = sp.getAssertionConsumerServiceManager().getEndpoints();
456 while (endpoints.hasNext()) {
457 if (shireURL.equals(((Endpoint) endpoints.next()).getLocation())) { return true; }
459 log.info("Supplied consumer URL not found in metadata.");