Began major refactoring of IdP servlet in order to allow support for multiple protoco...
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / idp / provider / SAMLv1_AttributeQueryHandler.java
1 /*
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.
24  */
25
26 package edu.internet2.middleware.shibboleth.idp.provider;
27
28 import java.io.IOException;
29 import java.net.URI;
30 import java.net.URISyntaxException;
31 import java.security.Principal;
32 import java.security.cert.X509Certificate;
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.Collections;
36 import java.util.Date;
37 import java.util.Iterator;
38
39 import javax.security.auth.x500.X500Principal;
40 import javax.servlet.ServletException;
41 import javax.servlet.http.HttpServletRequest;
42 import javax.servlet.http.HttpServletResponse;
43
44 import org.apache.log4j.Logger;
45 import org.opensaml.SAMLAssertion;
46 import org.opensaml.SAMLAttribute;
47 import org.opensaml.SAMLAttributeDesignator;
48 import org.opensaml.SAMLAttributeQuery;
49 import org.opensaml.SAMLAttributeStatement;
50 import org.opensaml.SAMLAudienceRestrictionCondition;
51 import org.opensaml.SAMLCondition;
52 import org.opensaml.SAMLException;
53 import org.opensaml.SAMLRequest;
54 import org.opensaml.SAMLResponse;
55 import org.opensaml.SAMLStatement;
56 import org.opensaml.SAMLSubject;
57
58 import sun.misc.BASE64Decoder;
59 import edu.internet2.middleware.shibboleth.common.RelyingParty;
60 import edu.internet2.middleware.shibboleth.common.ShibBrowserProfile;
61 import edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler;
62 import edu.internet2.middleware.shibboleth.idp.IdPProtocolSupport;
63 import edu.internet2.middleware.shibboleth.idp.InvalidClientDataException;
64 import edu.internet2.middleware.shibboleth.metadata.EntityDescriptor;
65
66 /**
67  * @author Walter Hoehn
68  */
69 public class SAMLv1_AttributeQueryHandler extends BaseServiceHandler implements IdPProtocolHandler {
70
71         private static Logger log = Logger.getLogger(SAMLv1_AttributeQueryHandler.class.getName());
72
73         /*
74          * @see edu.internet2.middleware.shibboleth.idp.ProtocolHandler#getHandlerName()
75          */
76         public String getHandlerName() {
77
78                 return "SAML v1.1 Attribute Query";
79         }
80
81         private String getEffectiveName(HttpServletRequest req, RelyingParty relyingParty)
82                         throws InvalidProviderCredentialException {
83
84                 // X500Principal credentialName = getCredentialName(req);
85                 X509Certificate credential = getCredentialFromProvider(req);
86
87                 if (credential == null || credential.getSubjectX500Principal().getName(X500Principal.RFC2253).equals("")) {
88                         log.info("Request is from an unauthenticated service provider.");
89                         return null;
90
91                 } else {
92                         log.info("Request contains credential: ("
93                                         + credential.getSubjectX500Principal().getName(X500Principal.RFC2253) + ").");
94                         // Mockup old requester name for requests from < 1.2 targets
95                         if (fromLegacyProvider(req)) {
96                                 String legacyName = ShibBrowserProfile.getHostNameFromDN(credential.getSubjectX500Principal());
97                                 if (legacyName == null) {
98                                         log.error("Unable to extract legacy requester name from certificate subject.");
99                                 }
100
101                                 log.info("Request from legacy service provider: (" + legacyName + ").");
102                                 return legacyName;
103
104                         } else {
105
106                                 // See if we have metadata for this provider
107                                 EntityDescriptor provider = protocolSupport.lookup(relyingParty.getProviderId());
108                                 if (provider == null) {
109                                         log.info("No metadata found for provider: (" + relyingParty.getProviderId() + ").");
110                                         log.info("Treating remote provider as unauthenticated.");
111                                         return null;
112                                 }
113
114                                 // Make sure that the suppplied credential is valid for the
115                                 // selected relying party
116                                 if (isValidCredential(provider, credential)) {
117                                         log.info("Supplied credential validated for this provider.");
118                                         log.info("Request from service provider: (" + relyingParty.getProviderId() + ").");
119                                         return relyingParty.getProviderId();
120                                 } else {
121                                         log.error("Supplied credential ("
122                                                         + credential.getSubjectX500Principal().getName(X500Principal.RFC2253)
123                                                         + ") is NOT valid for provider (" + relyingParty.getProviderId() + ").");
124                                         throw new InvalidProviderCredentialException("Invalid credential.");
125                                 }
126                         }
127                 }
128         }
129
130         /*
131          * @see edu.internet2.middleware.shibboleth.idp.ProtocolHandler#processRequest(javax.servlet.http.HttpServletRequest,
132          *      javax.servlet.http.HttpServletResponse, org.opensaml.SAMLRequest,
133          *      edu.internet2.middleware.shibboleth.idp.ProtocolSupport)
134          */
135         public SAMLResponse processRequest(HttpServletRequest request, HttpServletResponse response,
136                         SAMLRequest samlRequest, IdPProtocolSupport support) throws SAMLException, InvalidClientDataException,
137                         IOException, ServletException {
138
139                 // TODO negate this and throw an error if it isn't
140                 if (samlRequest.getQuery() != null && (samlRequest.getQuery() instanceof SAMLAttributeQuery)) {
141                         log.info("Recieved an attribute query.");
142                         // processAttributeQuery(samlRequest, request, response);
143
144                 }
145
146                 RelyingParty relyingParty = null;
147
148                 SAMLAttributeQuery attributeQuery = (SAMLAttributeQuery) samlRequest.getQuery();
149
150                 if (!fromLegacyProvider(request)) {
151                         log.info("Remote provider has identified itself as: (" + attributeQuery.getResource() + ").");
152                 }
153
154                 // This is the requester name that will be passed to subsystems
155                 String effectiveName = null;
156
157                 X509Certificate credential = getCredentialFromProvider(request);
158                 if (credential == null || credential.getSubjectX500Principal().getName(X500Principal.RFC2253).equals("")) {
159                         log.info("Request is from an unauthenticated service provider.");
160                 } else {
161
162                         // Identify a Relying Party
163                         relyingParty = support.getServiceProviderMapper().getRelyingParty(attributeQuery.getResource());
164
165                         try {
166                                 effectiveName = getEffectiveName(request, relyingParty);
167                         } catch (InvalidProviderCredentialException ipc) {
168                                 sendSAMLFailureResponse(response, samlRequest, new SAMLException(SAMLException.RESPONDER,
169                                                 "Invalid credentials for request."));
170                                 return null;
171                         }
172                 }
173
174                 if (effectiveName == null) {
175                         log.debug("Using default Relying Party for unauthenticated provider.");
176                         relyingParty = support.getServiceProviderMapper().getRelyingParty(null);
177                 }
178
179                 // Fail if we can't honor SAML Subject Confirmation
180                 if (!fromLegacyProvider(request)) {
181                         Iterator iterator = attributeQuery.getSubject().getConfirmationMethods();
182                         boolean hasConfirmationMethod = false;
183                         while (iterator.hasNext()) {
184                                 log.info("Request contains SAML Subject Confirmation method: (" + (String) iterator.next() + ").");
185                         }
186                         if (hasConfirmationMethod) { throw new SAMLException(SAMLException.REQUESTER,
187                                         "This SAML authority cannot honor requests containing the supplied SAML Subject Confirmation Method."); }
188                 }
189
190                 // Map Subject to local principal
191                 Principal principal = support.getNameMapper().getPrincipal(attributeQuery.getSubject().getName(), relyingParty,
192                                 relyingParty.getIdentityProvider());
193                 log.info("Request is for principal (" + principal.getName() + ").");
194
195                 SAMLAttribute[] attrs;
196                 Iterator requestedAttrsIterator = attributeQuery.getDesignators();
197                 if (requestedAttrsIterator.hasNext()) {
198                         log.info("Request designates specific attributes, resolving this set.");
199                         ArrayList requestedAttrs = new ArrayList();
200                         while (requestedAttrsIterator.hasNext()) {
201                                 SAMLAttributeDesignator attribute = (SAMLAttributeDesignator) requestedAttrsIterator.next();
202                                 try {
203                                         log.debug("Designated attribute: (" + attribute.getName() + ")");
204                                         requestedAttrs.add(new URI(attribute.getName()));
205                                 } catch (URISyntaxException use) {
206                                         log.error("Request designated an attribute name that does not conform to the required URI syntax ("
207                                                         + attribute.getName() + ").  Ignoring this attribute");
208                                 }
209                         }
210
211                         attrs = support.getReleaseAttributes(principal, effectiveName, null, (URI[]) requestedAttrs
212                                         .toArray(new URI[0]));
213                 } else {
214                         log.info("Request does not designate specific attributes, resolving all available.");
215                         attrs = support.getReleaseAttributes(principal, effectiveName, null);
216                 }
217
218                 log.info("Found " + attrs.length + " attribute(s) for " + principal.getName());
219                 sendSAMLResponse(response, attrs, samlRequest, relyingParty, null);
220                 log.info("Successfully responded about " + principal.getName());
221
222                 if (effectiveName == null) {
223                         if (fromLegacyProvider(request)) {
224                                 support.getTransactionLog().info(
225                                                 "Attribute assertion issued to anonymous legacy provider at (" + request.getRemoteAddr()
226                                                                 + ") on behalf of principal (" + principal.getName() + ").");
227                         } else {
228                                 support.getTransactionLog().info(
229                                                 "Attribute assertion issued to anonymous provider at (" + request.getRemoteAddr()
230                                                                 + ") on behalf of principal (" + principal.getName() + ").");
231                         }
232                 } else {
233                         if (fromLegacyProvider(request)) {
234                                 support.getTransactionLog().info(
235                                                 "Attribute assertion issued to legacy provider (" + effectiveName
236                                                                 + ") on behalf of principal (" + principal.getName() + ").");
237                         } else {
238                                 support.getTransactionLog().info(
239                                                 "Attribute assertion issued to provider (" + effectiveName + ") on behalf of principal ("
240                                                                 + principal.getName() + ").");
241                         }
242                 }
243
244                 // TODO not NULL!!!
245                 return null;
246
247                 /*
248                  * throw new SAMLException(SAMLException.REQUESTER, "Identity Provider unable to respond to this SAML Request
249                  * type."); } catch (InvalidNameIdentifierException invalidNameE) { log.info("Could not associate the request
250                  * subject with a principal: " + invalidNameE); try { // TODO once again, ifgure out passThruErrors if (false) { //
251                  * if (relyingParty.passThruErrors()) { sendSAMLFailureResponse(response, samlRequest, new
252                  * SAMLException(Arrays.asList(invalidNameE .getSAMLErrorCodes()), "The supplied Subject was unrecognized.",
253                  * invalidNameE)); } else { sendSAMLFailureResponse(response, samlRequest, new
254                  * SAMLException(Arrays.asList(invalidNameE .getSAMLErrorCodes()), "The supplied Subject was unrecognized.")); }
255                  * return; } catch (Exception ee) { log.fatal("Could not construct a SAML error response: " + ee); throw new
256                  * ServletException("Identity Provider response failure."); }
257                  */
258
259         }
260
261         // TODO this should be renamed, since it is now only one type of response
262         // that we can send
263         public void sendSAMLResponse(HttpServletResponse resp, SAMLAttribute[] attrs, SAMLRequest samlRequest,
264                         RelyingParty relyingParty, SAMLException exception) throws IOException {
265
266                 SAMLException ourSE = null;
267                 SAMLResponse samlResponse = null;
268
269                 try {
270                         if (attrs == null || attrs.length == 0) {
271                                 // No attribute found
272                                 samlResponse = new SAMLResponse(samlRequest.getId(), null, null, exception);
273                         } else {
274
275                                 SAMLAttributeQuery attributeQuery = (SAMLAttributeQuery) samlRequest.getQuery();
276
277                                 // Reference requested subject
278                                 SAMLSubject rSubject = (SAMLSubject) attributeQuery.getSubject().clone();
279
280                                 // Set appropriate audience
281                                 ArrayList audiences = new ArrayList();
282                                 if (relyingParty.getProviderId() != null) {
283                                         audiences.add(relyingParty.getProviderId());
284                                 }
285                                 if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
286                                         audiences.add(relyingParty.getName());
287                                 }
288                                 SAMLCondition condition = new SAMLAudienceRestrictionCondition(audiences);
289
290                                 // Put all attributes into an assertion
291                                 SAMLStatement statement = new SAMLAttributeStatement(rSubject, Arrays.asList(attrs));
292
293                                 // Set assertion expiration to longest attribute expiration
294                                 long max = 0;
295                                 for (int i = 0; i < attrs.length; i++) {
296                                         if (max < attrs[i].getLifetime()) {
297                                                 max = attrs[i].getLifetime();
298                                         }
299                                 }
300                                 Date now = new Date();
301                                 Date then = new Date(now.getTime() + (max * 1000)); // max is in
302                                 // seconds
303
304                                 SAMLAssertion sAssertion = new SAMLAssertion(relyingParty.getIdentityProvider().getProviderId(), now,
305                                                 then, Collections.singleton(condition), null, Collections.singleton(statement));
306
307                                 samlResponse = new SAMLResponse(samlRequest.getId(), null, Collections.singleton(sAssertion), exception);
308                                 ProtocolSupport.addSignatures(samlResponse, relyingParty, protocolSupport.lookup(relyingParty
309                                                 .getProviderId()), false);
310                         }
311                 } catch (SAMLException se) {
312                         ourSE = se;
313                 } catch (CloneNotSupportedException ex) {
314                         ourSE = new SAMLException(SAMLException.RESPONDER, ex);
315
316                 } finally {
317
318                         if (log.isDebugEnabled()) { // This takes some processing, so only do it if we need to
319                                 try {
320                                         log.debug("Dumping generated SAML Response:"
321                                                         + System.getProperty("line.separator")
322                                                         + new String(
323                                                                         new BASE64Decoder().decodeBuffer(new String(samlResponse.toBase64(), "ASCII")),
324                                                                         "UTF8"));
325                                 } catch (SAMLException e) {
326                                         log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
327                                 } catch (IOException e) {
328                                         log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
329                                 }
330                         }
331
332                         try {
333                                 binding.respond(resp, samlResponse, ourSE);
334                         } catch (SAMLException e) {
335                                 log.error("Caught exception while responding to requester: " + e.getMessage());
336                                 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Error while responding.");
337                         }
338                 }
339         }
340
341         private static boolean fromLegacyProvider(HttpServletRequest request) {
342
343                 String version = request.getHeader("Shibboleth");
344                 if (version != null) {
345                         log.debug("Request from Shibboleth version: " + version);
346                         return false;
347                 }
348                 log.debug("No version header found.");
349                 return true;
350         }
351
352 }