2 * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package edu.internet2.middleware.shibboleth.idp.provider;
19 import java.io.IOException;
20 import java.io.UnsupportedEncodingException;
21 import java.net.URLEncoder;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.Collection;
25 import java.util.Collections;
26 import java.util.Date;
27 import java.util.Iterator;
28 import java.util.List;
29 import java.util.Vector;
31 import javax.servlet.RequestDispatcher;
32 import javax.servlet.ServletException;
33 import javax.servlet.http.HttpServletRequest;
34 import javax.servlet.http.HttpServletResponse;
36 import org.apache.log4j.Logger;
37 import org.bouncycastle.util.encoders.Base64;
38 import org.opensaml.SAMLAssertion;
39 import org.opensaml.SAMLAttribute;
40 import org.opensaml.SAMLAttributeStatement;
41 import org.opensaml.SAMLAudienceRestrictionCondition;
42 import org.opensaml.SAMLAuthenticationStatement;
43 import org.opensaml.SAMLAuthorityBinding;
44 import org.opensaml.SAMLBrowserProfile;
45 import org.opensaml.SAMLCondition;
46 import org.opensaml.SAMLException;
47 import org.opensaml.SAMLNameIdentifier;
48 import org.opensaml.SAMLResponse;
49 import org.opensaml.SAMLStatement;
50 import org.opensaml.SAMLSubject;
51 import org.opensaml.SAMLSubjectStatement;
52 import org.opensaml.artifact.Artifact;
53 import org.opensaml.saml2.metadata.AssertionConsumerService;
54 import org.opensaml.saml2.metadata.EntityDescriptor;
55 import org.opensaml.saml2.metadata.SPSSODescriptor;
56 import org.opensaml.saml2.metadata.provider.MetadataProviderException;
57 import org.w3c.dom.Element;
59 import edu.internet2.middleware.shibboleth.aa.AAException;
60 import edu.internet2.middleware.shibboleth.common.LocalPrincipal;
61 import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException;
62 import edu.internet2.middleware.shibboleth.common.RelyingParty;
63 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
64 import edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler;
65 import edu.internet2.middleware.shibboleth.idp.IdPProtocolSupport;
66 import edu.internet2.middleware.shibboleth.idp.InvalidClientDataException;
67 import edu.internet2.middleware.shibboleth.idp.RequestHandlingException;
70 * <code>ProtocolHandler</code> implementation that responds to SSO flows as specified in "Shibboleth Architecture:
71 * Protocols and Profiles".
73 * @author Walter Hoehn
75 public class ShibbolethV1SSOHandler extends SSOHandler implements IdPProtocolHandler {
77 private static Logger log = Logger.getLogger(ShibbolethV1SSOHandler.class.getName());
80 * Required DOM-based constructor.
82 public ShibbolethV1SSOHandler(Element config) throws ShibbolethConfigurationException {
88 * @see edu.internet2.middleware.shibboleth.idp.IdPResponder.ProtocolHandler#processRequest(javax.servlet.http.HttpServletRequest,
89 * javax.servlet.http.HttpServletResponse)
91 public void processRequest(HttpServletRequest request, HttpServletResponse response, IdPProtocolSupport support)
92 throws RequestHandlingException, ServletException {
96 // Set attributes that are needed by the jsp
97 request.setAttribute("shire", request.getParameter("shire"));
98 request.setAttribute("target", request.getParameter("target"));
100 // Ensure that we have the required data from the servlet container
101 validateEngineData(request);
102 validateShibSpecificData(request);
104 // Get the authN info
105 String username = support.getIdPConfig().getAuthHeaderName().equalsIgnoreCase("REMOTE_USER") ? request
106 .getRemoteUser() : request.getHeader(support.getIdPConfig().getAuthHeaderName());
107 if ((username == null) || (username.equals(""))) { throw new InvalidClientDataException(
108 "Unauthenticated principal. This protocol handler requires that authentication information be "
109 + "provided from the servlet container."); }
110 LocalPrincipal principal = new LocalPrincipal(username);
112 // Select the appropriate Relying Party configuration for the request
113 RelyingParty relyingParty = null;
114 String remoteProviderId = request.getParameter("providerId");
115 // If the SP did not send a Provider Id, then assume it is a Shib
117 if (remoteProviderId == null || remoteProviderId.equals("")) {
118 throw new InvalidClientDataException("Invalid or missing service provider id.");
120 log.debug("Remote provider has identified itself as: (" + remoteProviderId + ").");
121 relyingParty = support.getServiceProviderMapper().getRelyingParty(remoteProviderId);
124 // Grab the metadata for the provider
125 EntityDescriptor descriptor = null;
127 descriptor = support.getEntityDescriptor(relyingParty.getProviderId());
128 } catch (MetadataProviderException e1) {
129 log.error("Metadata lookup for provider (" + relyingParty.getProviderId() + ") encountered an error: "
133 // Make sure that the selected relying party configuration is appropriate for this
135 String acceptanceURL = request.getParameter("shire");
137 if (descriptor == null) {
138 log.info("No metadata found for provider: (" + relyingParty.getProviderId() + ").");
139 relyingParty = support.getServiceProviderMapper().getRelyingParty(null);
142 if (isValidAssertionConsumerURL(descriptor, acceptanceURL)) {
143 log.info("Supplied consumer URL validated for this provider.");
145 log.error("Assertion consumer service URL (" + acceptanceURL + ") is NOT valid for provider ("
146 + relyingParty.getProviderId() + ").");
147 throw new InvalidClientDataException("Invalid assertion consumer service URL.");
151 // Create SAML Name Identifier & Subject
152 SAMLNameIdentifier nameId;
154 nameId = getNameIdentifier(support.getNameMapper(), principal, relyingParty, descriptor);
156 String authenticationMethod = request.getHeader("SAMLAuthenticationMethod");
157 if (authenticationMethod == null || authenticationMethod.equals("")) {
158 authenticationMethod = relyingParty.getDefaultAuthMethod().toString();
159 log.debug("User was authenticated via the default method for this relying party ("
160 + authenticationMethod + ").");
162 log.debug("User was authenticated via the method (" + authenticationMethod + ").");
165 SAMLSubject authNSubject = new SAMLSubject(nameId, null, null, null);
167 // Is this artifact or POST?
168 boolean artifactProfile = useArtifactProfile(descriptor, acceptanceURL, relyingParty);
170 // SAML Artifact profile - don't even attempt this for legacy providers (they don't support it)
171 if (artifactProfile) {
172 respondWithArtifact(request, response, support, principal, relyingParty, descriptor, acceptanceURL,
173 nameId, authenticationMethod, authNSubject);
177 respondWithPOST(request, response, support, principal, relyingParty, descriptor, acceptanceURL, nameId,
178 authenticationMethod, authNSubject);
180 } catch (InvalidClientDataException e) {
181 throw new RequestHandlingException("Unable to handle request. Client data is invalid: " + e);
182 } catch (NameIdentifierMappingException e) {
183 log.error("Error converting principal to SAML Name Identifier: " + e);
184 throw new RequestHandlingException("Unable to handle request. Error recognizing principal.");
185 } catch (SAMLException e) {
186 log.error("Error creating SAML Response: " + e);
187 throw new RequestHandlingException("Unalbe to handle request. Error creating SAML Response.");
191 private void respondWithArtifact(HttpServletRequest request, HttpServletResponse response,
192 IdPProtocolSupport support, LocalPrincipal principal, RelyingParty relyingParty,
193 EntityDescriptor descriptor, String acceptanceURL, SAMLNameIdentifier nameId, String authenticationMethod,
194 SAMLSubject authNSubject) throws SAMLException, ServletException, RequestHandlingException {
196 log.debug("Responding with Artifact profile.");
197 ArrayList<SAMLAssertion> assertions = new ArrayList<SAMLAssertion>();
199 authNSubject.addConfirmationMethod(SAMLSubject.CONF_ARTIFACT);
200 assertions.add(generateAuthNAssertion(request, relyingParty, descriptor, nameId, authenticationMethod,
201 getAuthNTime(request), authNSubject));
203 // Package attributes for push, if necessary.
204 if (pushAttributes(true, relyingParty)) {
205 log.info("Resolving attributes for push.");
206 generateAttributes(support, principal, relyingParty, assertions, request);
209 // Sign the assertions, if necessary
210 boolean metaDataIndicatesSignAssertions = false;
211 if (descriptor != null) {
212 SPSSODescriptor sp = descriptor.getSPSSODescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
214 if (sp.getWantAssertionsSigned()) {
215 metaDataIndicatesSignAssertions = true;
219 if (relyingParty.wantsAssertionsSigned() || metaDataIndicatesSignAssertions) {
220 support.signAssertions((SAMLAssertion[]) assertions.toArray(new SAMLAssertion[0]), relyingParty);
223 // Create artifacts for each assertion
224 ArrayList<Artifact> artifacts = new ArrayList<Artifact>();
225 for (int i = 0; i < assertions.size(); i++) {
226 SAMLAssertion assertion = (SAMLAssertion) assertions.get(i);
227 Artifact artifact = support.getArtifactMapper().generateArtifact(assertion, relyingParty);
228 artifacts.add(artifact);
230 // Put attributes names in the transaction log when it is set to DEBUG
231 if (support.getTransactionLog().isDebugEnabled()) {
232 Iterator statements = assertion.getStatements();
233 while (statements.hasNext()) {
234 SAMLStatement statement = (SAMLStatement) statements.next();
235 if (statement instanceof SAMLAttributeStatement) {
236 Iterator attributes = ((SAMLAttributeStatement) statement).getAttributes();
237 StringBuffer attributeBuffer = new StringBuffer();
238 while (attributes.hasNext()) {
239 SAMLAttribute attribute = (SAMLAttribute) attributes.next();
240 attributeBuffer.append("(" + attribute.getName() + ")");
241 support.getTransactionLog().debug(
242 "Artifact (" + artifact.encode() + ") created with the following attributes: "
243 + attributeBuffer.toString());
251 // Assemble the query string
252 StringBuffer destination = new StringBuffer(acceptanceURL);
253 destination.append("?TARGET=");
255 destination.append(URLEncoder.encode(request.getParameter("target"), "UTF-8"));
257 Iterator iterator = artifacts.iterator();
258 StringBuffer artifactBuffer = new StringBuffer(); // Buffer for the transaction log
260 // Construct the artifact query parameter
261 while (iterator.hasNext()) {
262 Artifact artifact = (Artifact) iterator.next();
263 artifactBuffer.append("(" + artifact.encode() + ")");
264 destination.append("&SAMLart=");
265 destination.append(URLEncoder.encode(artifact.encode(), "UTF-8"));
268 log.debug("Redirecting to (" + destination.toString() + ").");
270 response.sendRedirect(destination.toString());
272 // Redirect to the artifact receiver
273 support.getTransactionLog().info(
274 "Assertion artifact(s) (" + artifactBuffer.toString() + ") issued to provider ("
275 + relyingParty.getProviderId() + ") on behalf of principal (" + principal.getName()
276 + "). Name Identifier: (" + nameId.getName() + "). Name Identifier Format: ("
277 + nameId.getFormat() + ").");
278 } catch (UnsupportedEncodingException e) {
279 log.error("Error encoding URL: " + e);
280 throw new RequestHandlingException("Unable to handle request. URL Encoder malfuntion.");
281 } catch (IOException e) {
282 log.error("Error issuing redirect: " + e);
283 throw new ServletException(e);
287 public static boolean pushAttributeDefault = false;
289 private void respondWithPOST(HttpServletRequest request, HttpServletResponse response, IdPProtocolSupport support,
290 LocalPrincipal principal, RelyingParty relyingParty, EntityDescriptor descriptor, String acceptanceURL,
291 SAMLNameIdentifier nameId, String authenticationMethod, SAMLSubject authNSubject) throws SAMLException,
294 log.debug("Responding with POST profile.");
295 ArrayList<SAMLAssertion> assertions = new ArrayList<SAMLAssertion>();
296 authNSubject.addConfirmationMethod(SAMLSubject.CONF_BEARER);
297 assertions.add(generateAuthNAssertion(request, relyingParty, descriptor, nameId, authenticationMethod,
298 getAuthNTime(request), authNSubject));
300 // Package attributes for push, if necessary.
301 if (pushAttributes(pushAttributeDefault, relyingParty)) {
302 log.info("Resolving attributes for push.");
303 generateAttributes(support, principal, relyingParty, assertions, request);
306 // Sign the assertions, if necessary
307 boolean metaDataIndicatesSignAssertions = false;
308 if (descriptor != null) {
309 SPSSODescriptor sp = descriptor.getSPSSODescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
311 if (sp.getWantAssertionsSigned()) {
312 metaDataIndicatesSignAssertions = true;
316 if (relyingParty.wantsAssertionsSigned() || metaDataIndicatesSignAssertions) {
317 support.signAssertions((SAMLAssertion[]) assertions.toArray(new SAMLAssertion[0]), relyingParty);
320 // Set attributes needed by form
321 request.setAttribute("acceptanceURL", acceptanceURL);
322 request.setAttribute("target", request.getParameter("target"));
324 SAMLResponse samlResponse = new SAMLResponse(null, acceptanceURL, assertions, null);
326 support.signResponse(samlResponse, relyingParty);
329 createPOSTForm(request, response, samlResponse.toBase64());
330 } catch (IOException e) {
331 log.error("Error creating POST Form: " + e);
332 throw new ServletException(e);
335 // Make transaction log entry
336 support.getTransactionLog().info(
337 "Authentication assertion issued to provider (" + relyingParty.getProviderId()
338 + ") on behalf of principal (" + principal.getName() + "). Name Identifier: ("
339 + nameId.getName() + "). Name Identifier Format: (" + nameId.getFormat() + ").");
343 private void generateAttributes(IdPProtocolSupport support, LocalPrincipal principal, RelyingParty relyingParty,
344 ArrayList<SAMLAssertion> assertions, HttpServletRequest request) throws SAMLException {
347 Collection<? extends SAMLAttribute> attributes = support.getReleaseAttributes(principal, relyingParty,
348 relyingParty.getProviderId());
349 log.info("Found " + attributes.size() + " attribute(s) for " + principal.getName());
351 // Bail if we didn't get any attributes
352 if (attributes == null || attributes.size() < 1) {
353 log.info("No attributes resolved.");
357 // Reference requested subject
358 SAMLSubject attrSubject = (SAMLSubject) ((SAMLSubjectStatement) ((SAMLAssertion) assertions.get(0))
359 .getStatements().next()).getSubject().clone();
361 // May be one assertion or two.
362 if (relyingParty.singleAssertion()) {
363 log.debug("merging attributes into existing authn assertion");
364 // Put all attributes into an assertion
365 ((SAMLAssertion) assertions.get(0)).addStatement(new SAMLAttributeStatement(attrSubject, Arrays
366 .asList(attributes)));
368 if (log.isDebugEnabled()) {
369 log.debug("Dumping combined Assertion:" + System.getProperty("line.separator")
370 + assertions.get(0).toString());
373 ArrayList<String> audiences = new ArrayList<String>();
374 if (relyingParty.getProviderId() != null) {
375 audiences.add(relyingParty.getProviderId());
377 if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
378 audiences.add(relyingParty.getName());
380 String remoteProviderId = request.getParameter("providerId");
381 if (remoteProviderId != null && !remoteProviderId.equals("") && !audiences.contains(remoteProviderId)) {
382 audiences.add(remoteProviderId);
385 SAMLCondition condition = new SAMLAudienceRestrictionCondition(audiences);
387 // Put all attributes into an assertion
388 SAMLStatement statement = new SAMLAttributeStatement(attrSubject, attributes);
390 // Set assertion expiration to longest attribute expiration
392 for (SAMLAttribute attribute : attributes) {
393 if (max < attribute.getLifetime()) {
394 max = attribute.getLifetime();
397 Date now = new Date();
398 Date then = new Date(now.getTime() + (max * 1000)); // max is in seconds
400 SAMLAssertion attrAssertion = new SAMLAssertion(relyingParty.getIdentityProvider().getProviderId(),
401 now, then, Collections.singleton(condition), null, Collections.singleton(statement));
402 assertions.add(attrAssertion);
404 if (log.isDebugEnabled()) {
405 log.debug("Dumping generated Attribute Assertion:" + System.getProperty("line.separator")
406 + attrAssertion.toString());
409 } catch (AAException e) {
410 log.error("An error was encountered while generating assertion for attribute push: " + e);
411 throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
412 } catch (CloneNotSupportedException e) {
413 log.error("An error was encountered while generating assertion for attribute push: " + e);
414 throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
418 private SAMLAssertion generateAuthNAssertion(HttpServletRequest request, RelyingParty relyingParty,
419 EntityDescriptor descriptor, SAMLNameIdentifier nameId, String authenticationMethod, Date authTime,
420 SAMLSubject subject) throws SAMLException {
422 // Determine the correct audiences
423 ArrayList<String> audiences = new ArrayList<String>();
424 if (relyingParty.getProviderId() != null) {
425 audiences.add(relyingParty.getProviderId());
427 if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
428 audiences.add(relyingParty.getName());
430 String remoteProviderId = request.getParameter("providerId");
431 if (remoteProviderId != null && !remoteProviderId.equals("") && !audiences.contains(remoteProviderId)) {
432 audiences.add(remoteProviderId);
435 // Determine the correct issuer
436 String issuer = relyingParty.getIdentityProvider().getProviderId();
438 ArrayList<SAMLAuthorityBinding> bindings = new ArrayList<SAMLAuthorityBinding>();
440 // Create the assertion
441 Vector<SAMLCondition> conditions = new Vector<SAMLCondition>(1);
442 if (audiences != null && audiences.size() > 0) conditions.add(new SAMLAudienceRestrictionCondition(audiences));
444 SAMLStatement[] statements = {new SAMLAuthenticationStatement(subject, authenticationMethod, authTime, request
445 .getRemoteAddr(), null, bindings)};
447 SAMLAssertion assertion = new SAMLAssertion(issuer, new Date(System.currentTimeMillis()), new Date(System
448 .currentTimeMillis() + 300000), conditions, null, Arrays.asList(statements));
450 if (log.isDebugEnabled()) {
451 log.debug("Dumping generated AuthN Assertion:" + System.getProperty("line.separator")
452 + assertion.toString());
459 * @see edu.internet2.middleware.shibboleth.idp.IdPResponder.ProtocolHandler#getHandlerName()
461 public String getHandlerName() {
463 return "Shibboleth v1.x SSO";
466 private void validateShibSpecificData(HttpServletRequest request) throws InvalidClientDataException {
468 if (request.getParameter("target") == null || request.getParameter("target").equals("")) { throw new InvalidClientDataException(
469 "Invalid data from Service Provider: no target URL received."); }
470 if ((request.getParameter("shire") == null) || (request.getParameter("shire").equals(""))) { throw new InvalidClientDataException(
471 "Invalid data from Service Provider: No acceptance URL received."); }
474 private static void createPOSTForm(HttpServletRequest req, HttpServletResponse res, byte[] buf) throws IOException,
477 // Hardcoded to ASCII to ensure Base64 encoding compatibility
478 req.setAttribute("assertion", new String(buf, "ASCII"));
480 if (log.isDebugEnabled()) {
481 log.debug("Dumping generated SAML Response:" + System.getProperty("line.separator")
482 + new String(Base64.decode(buf)));
485 RequestDispatcher rd = req.getRequestDispatcher("/IdP.jsp");
486 rd.forward(req, res);
490 * Boolean indication of which browser profile is in effect. "true" indicates Artifact and "false" indicates POST.
492 private static boolean useArtifactProfile(EntityDescriptor descriptor, String acceptanceURL,
493 RelyingParty relyingParty) {
495 boolean artifactMeta = false;
496 boolean postMeta = false;
498 // Look at the metadata bindings, if we can find them
499 if (descriptor != null) {
500 SPSSODescriptor sp = descriptor.getSPSSODescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
504 // See if this is the default endpoint location.
505 AssertionConsumerService defaultEndpoint = sp.getDefaultAssertionConsumerService();
506 if (defaultEndpoint != null && defaultEndpoint.getLocation().equals(acceptanceURL)) {
507 // If we recognize the default binding, this is the one to use.
508 if (defaultEndpoint.getBinding().equals(SAMLBrowserProfile.PROFILE_POST_URI)) return false;
509 else if (defaultEndpoint.getBinding().equals(SAMLBrowserProfile.PROFILE_ARTIFACT_URI)) return true;
511 // If not, look through everything we have
512 List<AssertionConsumerService> endpoints = sp.getAssertionConsumerServices();
513 for (AssertionConsumerService ep : endpoints) {
514 if (acceptanceURL.equals(ep.getLocation())
515 && SAMLBrowserProfile.PROFILE_POST_URI.equals(ep.getBinding())) {
516 log.debug("Metadata indicates support for POST profile.");
522 endpoints = sp.getAssertionConsumerServices();
523 for (AssertionConsumerService ep : endpoints) {
524 if (acceptanceURL.equals(ep.getLocation())
525 && SAMLBrowserProfile.PROFILE_ARTIFACT_URI.equals(ep.getBinding())) {
526 log.debug("Metadata indicates support for Artifact profile.");
534 // If we have metadata for both, use the relying party default
535 if (!(artifactMeta && postMeta)) {
537 // If we only have metadata for one, use it
538 if (artifactMeta) { return true; }
539 if (postMeta) { return false; }
543 // If we have missing or incomplete metadata, use relying party default
544 if (relyingParty.defaultToPOSTProfile()) {
552 * Boolean indication of whether an assertion containing an attribute statement should be bundled in the response
553 * with the assertion containing the AuthN statement.
555 private static boolean pushAttributes(boolean artifactProfile, RelyingParty relyingParty) {
557 // By default push for Artifact and don't push for POST
558 // This can be overriden at the level of the relying party
559 if (relyingParty.forceAttributePush()) {
561 } else if (relyingParty.forceAttributeNoPush()) {
563 } else if (artifactProfile) {
571 * Boolean indication of whethere or not a given assertion consumer URL is valid for a given SP.
573 private static boolean isValidAssertionConsumerURL(EntityDescriptor descriptor, String shireURL)
574 throws InvalidClientDataException {
576 SPSSODescriptor sp = descriptor.getSPSSODescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
578 log.info("Inappropriate metadata for provider.");
582 List<AssertionConsumerService> endpoints = sp.getAssertionConsumerServices();
583 for (AssertionConsumerService endpoint : endpoints) {
584 if (shireURL.equals(endpoint.getLocation())) { return true; }
586 log.info("Supplied consumer URL not found in metadata.");