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.SAMLRequest;
49 import org.opensaml.SAMLResponse;
50 import org.opensaml.SAMLStatement;
51 import org.opensaml.SAMLSubject;
52 import org.opensaml.SAMLSubjectStatement;
53 import org.opensaml.artifact.Artifact;
54 import org.opensaml.saml2.metadata.AssertionConsumerService;
55 import org.opensaml.saml2.metadata.EntityDescriptor;
56 import org.opensaml.saml2.metadata.SPSSODescriptor;
57 import org.opensaml.saml2.metadata.provider.MetadataProviderException;
58 import org.w3c.dom.Element;
60 import edu.internet2.middleware.shibboleth.aa.AAException;
61 import edu.internet2.middleware.shibboleth.common.LocalPrincipal;
62 import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException;
63 import edu.internet2.middleware.shibboleth.common.RelyingParty;
64 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
65 import edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler;
66 import edu.internet2.middleware.shibboleth.idp.IdPProtocolSupport;
67 import edu.internet2.middleware.shibboleth.idp.InvalidClientDataException;
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 SAMLResponse processRequest(HttpServletRequest request, HttpServletResponse response,
92 SAMLRequest samlRequest, IdPProtocolSupport support) throws SAMLException, ServletException, IOException {
94 if (request == null) {
95 log.error("Protocol Handler received a SAML Request, but is unable to handle it.");
96 throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
99 // Set attributes that are needed by the jsp
100 request.setAttribute("shire", request.getParameter("shire"));
101 request.setAttribute("target", request.getParameter("target"));
104 // Ensure that we have the required data from the servlet container
105 validateEngineData(request);
106 validateShibSpecificData(request);
108 // Get the authN info
109 String username = support.getIdPConfig().getAuthHeaderName().equalsIgnoreCase("REMOTE_USER") ? request
110 .getRemoteUser() : request.getHeader(support.getIdPConfig().getAuthHeaderName());
111 if ((username == null) || (username.equals(""))) { throw new InvalidClientDataException(
112 "Unauthenticated principal. This protocol handler requires that authentication information be "
113 + "provided from the servlet container."); }
114 LocalPrincipal principal = new LocalPrincipal(username);
116 // Select the appropriate Relying Party configuration for the request
117 RelyingParty relyingParty = null;
118 String remoteProviderId = request.getParameter("providerId");
119 // If the SP did not send a Provider Id, then assume it is a Shib
121 if (remoteProviderId == null || remoteProviderId.equals("")) {
122 throw new InvalidClientDataException("Invalid or missing service provider id.");
124 log.debug("Remote provider has identified itself as: (" + remoteProviderId + ").");
125 relyingParty = support.getServiceProviderMapper().getRelyingParty(remoteProviderId);
128 // Grab the metadata for the provider
129 EntityDescriptor descriptor = null;
131 descriptor = support.getEntityDescriptor(relyingParty.getProviderId());
132 } catch (MetadataProviderException e1) {
133 log.error("Metadata lookup for provider (" + relyingParty.getProviderId() + ") encountered an error: "
137 // Make sure that the selected relying party configuration is appropriate for this
139 String acceptanceURL = request.getParameter("shire");
141 if (descriptor == null) {
142 log.info("No metadata found for provider: (" + relyingParty.getProviderId() + ").");
143 relyingParty = support.getServiceProviderMapper().getRelyingParty(null);
146 if (isValidAssertionConsumerURL(descriptor, acceptanceURL)) {
147 log.info("Supplied consumer URL validated for this provider.");
149 log.error("Assertion consumer service URL (" + acceptanceURL + ") is NOT valid for provider ("
150 + relyingParty.getProviderId() + ").");
151 throw new InvalidClientDataException("Invalid assertion consumer service URL.");
155 // Create SAML Name Identifier & Subject
156 SAMLNameIdentifier nameId;
158 nameId = getNameIdentifier(support.getNameMapper(), principal, relyingParty, descriptor);
159 } catch (NameIdentifierMappingException e) {
160 log.error("Error converting principal to SAML Name Identifier: " + e);
161 throw new SAMLException("Error converting principal to SAML Name Identifier.", e);
164 String authenticationMethod = request.getHeader("SAMLAuthenticationMethod");
165 if (authenticationMethod == null || authenticationMethod.equals("")) {
166 authenticationMethod = relyingParty.getDefaultAuthMethod().toString();
167 log.debug("User was authenticated via the default method for this relying party ("
168 + authenticationMethod + ").");
170 log.debug("User was authenticated via the method (" + authenticationMethod + ").");
173 SAMLSubject authNSubject = new SAMLSubject(nameId, null, null, null);
175 // Is this artifact or POST?
176 boolean artifactProfile = useArtifactProfile(descriptor, acceptanceURL, relyingParty);
178 // SAML Artifact profile - don't even attempt this for legacy providers (they don't support it)
179 if (artifactProfile) {
180 respondWithArtifact(request, response, support, principal, relyingParty, descriptor, acceptanceURL,
181 nameId, authenticationMethod, authNSubject);
185 respondWithPOST(request, response, support, principal, relyingParty, descriptor, acceptanceURL, nameId,
186 authenticationMethod, authNSubject);
188 } catch (InvalidClientDataException e) {
189 throw new SAMLException(SAMLException.RESPONDER, e.getMessage());
194 private void respondWithArtifact(HttpServletRequest request, HttpServletResponse response,
195 IdPProtocolSupport support, LocalPrincipal principal, RelyingParty relyingParty,
196 EntityDescriptor descriptor, String acceptanceURL, SAMLNameIdentifier nameId, String authenticationMethod,
197 SAMLSubject authNSubject) throws SAMLException, IOException, UnsupportedEncodingException {
199 log.debug("Responding with Artifact profile.");
200 ArrayList<SAMLAssertion> assertions = new ArrayList<SAMLAssertion>();
202 authNSubject.addConfirmationMethod(SAMLSubject.CONF_ARTIFACT);
203 assertions.add(generateAuthNAssertion(request, relyingParty, descriptor, nameId, authenticationMethod,
204 getAuthNTime(request), authNSubject));
206 // Package attributes for push, if necessary.
207 if (pushAttributes(true, relyingParty)) {
208 log.info("Resolving attributes for push.");
209 generateAttributes(support, principal, relyingParty, assertions, request);
212 // Sign the assertions, if necessary
213 boolean metaDataIndicatesSignAssertions = false;
214 if (descriptor != null) {
215 SPSSODescriptor sp = descriptor.getSPSSODescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
217 if (sp.getWantAssertionsSigned()) {
218 metaDataIndicatesSignAssertions = true;
222 if (relyingParty.wantsAssertionsSigned() || metaDataIndicatesSignAssertions) {
223 support.signAssertions((SAMLAssertion[]) assertions.toArray(new SAMLAssertion[0]), relyingParty);
226 // Create artifacts for each assertion
227 ArrayList<Artifact> artifacts = new ArrayList<Artifact>();
228 for (int i = 0; i < assertions.size(); i++) {
229 SAMLAssertion assertion = (SAMLAssertion) assertions.get(i);
230 Artifact artifact = support.getArtifactMapper().generateArtifact(assertion, relyingParty);
231 artifacts.add(artifact);
233 // Put attributes names in the transaction log when it is set to DEBUG
234 if (support.getTransactionLog().isDebugEnabled()) {
235 Iterator statements = assertion.getStatements();
236 while (statements.hasNext()) {
237 SAMLStatement statement = (SAMLStatement) statements.next();
238 if (statement instanceof SAMLAttributeStatement) {
239 Iterator attributes = ((SAMLAttributeStatement) statement).getAttributes();
240 StringBuffer attributeBuffer = new StringBuffer();
241 while (attributes.hasNext()) {
242 SAMLAttribute attribute = (SAMLAttribute) attributes.next();
243 attributeBuffer.append("(" + attribute.getName() + ")");
244 support.getTransactionLog().debug(
245 "Artifact (" + artifact.encode() + ") created with the following attributes: "
246 + attributeBuffer.toString());
253 // Assemble the query string
254 StringBuffer destination = new StringBuffer(acceptanceURL);
255 destination.append("?TARGET=");
256 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() + ").");
269 response.sendRedirect(destination.toString()); // Redirect to the artifact receiver
270 support.getTransactionLog().info(
271 "Assertion artifact(s) (" + artifactBuffer.toString() + ") issued to provider ("
272 + relyingParty.getProviderId() + ") on behalf of principal (" + principal.getName()
273 + "). Name Identifier: (" + nameId.getName() + "). Name Identifier Format: ("
274 + nameId.getFormat() + ").");
277 public static boolean pushAttributeDefault = false;
279 private void respondWithPOST(HttpServletRequest request, HttpServletResponse response, IdPProtocolSupport support,
280 LocalPrincipal principal, RelyingParty relyingParty, EntityDescriptor descriptor, String acceptanceURL,
281 SAMLNameIdentifier nameId, String authenticationMethod, SAMLSubject authNSubject) throws SAMLException,
282 IOException, ServletException {
284 log.debug("Responding with POST profile.");
285 ArrayList<SAMLAssertion> assertions = new ArrayList<SAMLAssertion>();
286 authNSubject.addConfirmationMethod(SAMLSubject.CONF_BEARER);
287 assertions.add(generateAuthNAssertion(request, relyingParty, descriptor, nameId, authenticationMethod,
288 getAuthNTime(request), authNSubject));
290 // Package attributes for push, if necessary.
291 if (pushAttributes(pushAttributeDefault, relyingParty)) {
292 log.info("Resolving attributes for push.");
293 generateAttributes(support, principal, relyingParty, assertions, request);
296 // Sign the assertions, if necessary
297 boolean metaDataIndicatesSignAssertions = false;
298 if (descriptor != null) {
299 SPSSODescriptor sp = descriptor.getSPSSODescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
301 if (sp.getWantAssertionsSigned()) {
302 metaDataIndicatesSignAssertions = true;
306 if (relyingParty.wantsAssertionsSigned() || metaDataIndicatesSignAssertions) {
307 support.signAssertions((SAMLAssertion[]) assertions.toArray(new SAMLAssertion[0]), relyingParty);
310 // Set attributes needed by form
311 request.setAttribute("acceptanceURL", acceptanceURL);
312 request.setAttribute("target", request.getParameter("target"));
314 SAMLResponse samlResponse = new SAMLResponse(null, acceptanceURL, assertions, null);
316 support.signResponse(samlResponse, relyingParty);
318 createPOSTForm(request, response, samlResponse.toBase64());
320 // Make transaction log entry
321 support.getTransactionLog().info(
322 "Authentication assertion issued to provider (" + relyingParty.getProviderId()
323 + ") on behalf of principal (" + principal.getName() + "). Name Identifier: ("
324 + nameId.getName() + "). Name Identifier Format: (" + nameId.getFormat() + ").");
328 private void generateAttributes(IdPProtocolSupport support, LocalPrincipal principal, RelyingParty relyingParty,
329 ArrayList<SAMLAssertion> assertions, HttpServletRequest request) throws SAMLException {
332 Collection<? extends SAMLAttribute> attributes = support.getReleaseAttributes(principal, relyingParty,
333 relyingParty.getProviderId());
334 log.info("Found " + attributes.size() + " attribute(s) for " + principal.getName());
336 // Bail if we didn't get any attributes
337 if (attributes == null || attributes.size() < 1) {
338 log.info("No attributes resolved.");
342 // Reference requested subject
343 SAMLSubject attrSubject = (SAMLSubject) ((SAMLSubjectStatement) ((SAMLAssertion) assertions.get(0))
344 .getStatements().next()).getSubject().clone();
346 // May be one assertion or two.
347 if (relyingParty.singleAssertion()) {
348 log.debug("merging attributes into existing authn assertion");
349 // Put all attributes into an assertion
350 ((SAMLAssertion) assertions.get(0)).addStatement(new SAMLAttributeStatement(attrSubject, Arrays
351 .asList(attributes)));
353 if (log.isDebugEnabled()) {
354 log.debug("Dumping combined Assertion:" + System.getProperty("line.separator")
355 + assertions.get(0).toString());
358 ArrayList<String> audiences = new ArrayList<String>();
359 if (relyingParty.getProviderId() != null) {
360 audiences.add(relyingParty.getProviderId());
362 if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
363 audiences.add(relyingParty.getName());
365 String remoteProviderId = request.getParameter("providerId");
366 if (remoteProviderId != null && !remoteProviderId.equals("") && !audiences.contains(remoteProviderId)) {
367 audiences.add(remoteProviderId);
370 SAMLCondition condition = new SAMLAudienceRestrictionCondition(audiences);
372 // Put all attributes into an assertion
373 SAMLStatement statement = new SAMLAttributeStatement(attrSubject, attributes);
375 // Set assertion expiration to longest attribute expiration
377 for (SAMLAttribute attribute : attributes) {
378 if (max < attribute.getLifetime()) {
379 max = attribute.getLifetime();
382 Date now = new Date();
383 Date then = new Date(now.getTime() + (max * 1000)); // max is in seconds
385 SAMLAssertion attrAssertion = new SAMLAssertion(relyingParty.getIdentityProvider().getProviderId(),
386 now, then, Collections.singleton(condition), null, Collections.singleton(statement));
387 assertions.add(attrAssertion);
389 if (log.isDebugEnabled()) {
390 log.debug("Dumping generated Attribute Assertion:" + System.getProperty("line.separator")
391 + attrAssertion.toString());
394 } catch (AAException e) {
395 log.error("An error was encountered while generating assertion for attribute push: " + e);
396 throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
397 } catch (CloneNotSupportedException e) {
398 log.error("An error was encountered while generating assertion for attribute push: " + e);
399 throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
403 private SAMLAssertion generateAuthNAssertion(HttpServletRequest request, RelyingParty relyingParty,
404 EntityDescriptor descriptor, SAMLNameIdentifier nameId, String authenticationMethod, Date authTime,
405 SAMLSubject subject) throws SAMLException, IOException {
407 // Determine the correct audiences
408 ArrayList<String> audiences = new ArrayList<String>();
409 if (relyingParty.getProviderId() != null) {
410 audiences.add(relyingParty.getProviderId());
412 if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
413 audiences.add(relyingParty.getName());
415 String remoteProviderId = request.getParameter("providerId");
416 if (remoteProviderId != null && !remoteProviderId.equals("") && !audiences.contains(remoteProviderId)) {
417 audiences.add(remoteProviderId);
420 // Determine the correct issuer
421 String issuer = relyingParty.getIdentityProvider().getProviderId();
423 ArrayList<SAMLAuthorityBinding> bindings = new ArrayList<SAMLAuthorityBinding>();
425 // Create the assertion
426 Vector<SAMLCondition> conditions = new Vector<SAMLCondition>(1);
427 if (audiences != null && audiences.size() > 0) conditions.add(new SAMLAudienceRestrictionCondition(audiences));
429 SAMLStatement[] statements = {new SAMLAuthenticationStatement(subject, authenticationMethod, authTime, request
430 .getRemoteAddr(), null, bindings)};
432 SAMLAssertion assertion = new SAMLAssertion(issuer, new Date(System.currentTimeMillis()), new Date(System
433 .currentTimeMillis() + 300000), conditions, null, Arrays.asList(statements));
435 if (log.isDebugEnabled()) {
436 log.debug("Dumping generated AuthN Assertion:" + System.getProperty("line.separator")
437 + assertion.toString());
444 * @see edu.internet2.middleware.shibboleth.idp.IdPResponder.ProtocolHandler#getHandlerName()
446 public String getHandlerName() {
448 return "Shibboleth v1.x SSO";
451 private void validateShibSpecificData(HttpServletRequest request) throws InvalidClientDataException {
453 if (request.getParameter("target") == null || request.getParameter("target").equals("")) { throw new InvalidClientDataException(
454 "Invalid data from Service Provider: no target URL received."); }
455 if ((request.getParameter("shire") == null) || (request.getParameter("shire").equals(""))) { throw new InvalidClientDataException(
456 "Invalid data from Service Provider: No acceptance URL received."); }
459 private static void createPOSTForm(HttpServletRequest req, HttpServletResponse res, byte[] buf) throws IOException,
462 // Hardcoded to ASCII to ensure Base64 encoding compatibility
463 req.setAttribute("assertion", new String(buf, "ASCII"));
465 if (log.isDebugEnabled()) {
466 log.debug("Dumping generated SAML Response:" + System.getProperty("line.separator")
467 + new String(Base64.decode(buf)));
470 RequestDispatcher rd = req.getRequestDispatcher("/IdP.jsp");
471 rd.forward(req, res);
475 * Boolean indication of which browser profile is in effect. "true" indicates Artifact and "false" indicates POST.
477 private static boolean useArtifactProfile(EntityDescriptor descriptor, String acceptanceURL,
478 RelyingParty relyingParty) {
480 boolean artifactMeta = false;
481 boolean postMeta = false;
483 // Look at the metadata bindings, if we can find them
484 if (descriptor != null) {
485 SPSSODescriptor sp = descriptor.getSPSSODescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
489 // See if this is the default endpoint location.
490 AssertionConsumerService defaultEndpoint = sp.getDefaultAssertionConsumerService();
491 if (defaultEndpoint != null && defaultEndpoint.getLocation().equals(acceptanceURL)) {
492 // If we recognize the default binding, this is the one to use.
493 if (defaultEndpoint.getBinding().equals(SAMLBrowserProfile.PROFILE_POST_URI)) return false;
494 else if (defaultEndpoint.getBinding().equals(SAMLBrowserProfile.PROFILE_ARTIFACT_URI)) return true;
496 // If not, look through everything we have
497 List<AssertionConsumerService> endpoints = sp.getAssertionConsumerServices();
498 for (AssertionConsumerService ep : endpoints) {
499 if (acceptanceURL.equals(ep.getLocation())
500 && SAMLBrowserProfile.PROFILE_POST_URI.equals(ep.getBinding())) {
501 log.debug("Metadata indicates support for POST profile.");
507 endpoints = sp.getAssertionConsumerServices();
508 for (AssertionConsumerService ep : endpoints) {
509 if (acceptanceURL.equals(ep.getLocation())
510 && SAMLBrowserProfile.PROFILE_ARTIFACT_URI.equals(ep.getBinding())) {
511 log.debug("Metadata indicates support for Artifact profile.");
519 // If we have metadata for both, use the relying party default
520 if (!(artifactMeta && postMeta)) {
522 // If we only have metadata for one, use it
523 if (artifactMeta) { return true; }
524 if (postMeta) { return false; }
528 // If we have missing or incomplete metadata, use relying party default
529 if (relyingParty.defaultToPOSTProfile()) {
537 * Boolean indication of whether an assertion containing an attribute statement should be bundled in the response
538 * with the assertion containing the AuthN statement.
540 private static boolean pushAttributes(boolean artifactProfile, RelyingParty relyingParty) {
542 // By default push for Artifact and don't push for POST
543 // This can be overriden at the level of the relying party
544 if (relyingParty.forceAttributePush()) {
546 } else if (relyingParty.forceAttributeNoPush()) {
548 } else if (artifactProfile) {
556 * Boolean indication of whethere or not a given assertion consumer URL is valid for a given SP.
558 private static boolean isValidAssertionConsumerURL(EntityDescriptor descriptor, String shireURL)
559 throws InvalidClientDataException {
561 SPSSODescriptor sp = descriptor.getSPSSODescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
563 log.info("Inappropriate metadata for provider.");
567 List<AssertionConsumerService> endpoints = sp.getAssertionConsumerServices();
568 for (AssertionConsumerService endpoint : endpoints) {
569 if (shireURL.equals(endpoint.getLocation())) { return true; }
571 log.info("Supplied consumer URL not found in metadata.");