eaeb2cd736986d8c3dbf4dee1e32b7b91af38826
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / idp / provider / SAMLv1_AttributeQueryHandler.java
1 /*
2  * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package edu.internet2.middleware.shibboleth.idp.provider;
18
19 import java.io.IOException;
20 import java.net.URI;
21 import java.net.URISyntaxException;
22 import java.security.Principal;
23 import java.security.cert.X509Certificate;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Collection;
27 import java.util.Collections;
28 import java.util.Date;
29 import java.util.Iterator;
30
31 import javax.security.auth.x500.X500Principal;
32 import javax.servlet.ServletException;
33 import javax.servlet.http.HttpServletRequest;
34 import javax.servlet.http.HttpServletResponse;
35
36 import org.apache.log4j.Logger;
37 import org.opensaml.SAMLAssertion;
38 import org.opensaml.SAMLAttribute;
39 import org.opensaml.SAMLAttributeDesignator;
40 import org.opensaml.SAMLAttributeQuery;
41 import org.opensaml.SAMLAttributeStatement;
42 import org.opensaml.SAMLAudienceRestrictionCondition;
43 import org.opensaml.SAMLCondition;
44 import org.opensaml.SAMLException;
45 import org.opensaml.SAMLNameIdentifier;
46 import org.opensaml.SAMLRequest;
47 import org.opensaml.SAMLResponse;
48 import org.opensaml.SAMLStatement;
49 import org.opensaml.SAMLSubject;
50 import org.opensaml.XML;
51 import org.w3c.dom.Element;
52
53 import edu.internet2.middleware.shibboleth.aa.AAException;
54 import edu.internet2.middleware.shibboleth.common.InvalidNameIdentifierException;
55 import edu.internet2.middleware.shibboleth.common.NameIdentifierMapping;
56 import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException;
57 import edu.internet2.middleware.shibboleth.common.RelyingParty;
58 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
59 import edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler;
60 import edu.internet2.middleware.shibboleth.idp.IdPProtocolSupport;
61 import edu.internet2.middleware.shibboleth.metadata.AttributeRequesterDescriptor;
62 import edu.internet2.middleware.shibboleth.metadata.EntityDescriptor;
63 import edu.internet2.middleware.shibboleth.metadata.RoleDescriptor;
64 import edu.internet2.middleware.shibboleth.metadata.SPSSODescriptor;
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          * Required DOM-based constructor.
75          */
76         public SAMLv1_AttributeQueryHandler(Element config) throws ShibbolethConfigurationException {
77
78                 super(config);
79         }
80
81         /*
82          * @see edu.internet2.middleware.shibboleth.idp.ProtocolHandler#getHandlerName()
83          */
84         public String getHandlerName() {
85
86                 return "SAML v1.1 Attribute Query";
87         }
88
89         private String authenticateAs(String assertedId, X509Certificate[] chain, IdPProtocolSupport support)
90                         throws InvalidProviderCredentialException {
91
92                 // See if we have metadata for this provider
93                 EntityDescriptor provider = support.lookup(assertedId);
94                 if (provider == null) {
95                         log.info("No metadata found for providerId: (" + assertedId + ").");
96                         return null;
97                 } else {
98                         log.info("Metadata found for providerId: (" + assertedId + ").");
99                 }
100                 RoleDescriptor ar_role = provider.getAttributeRequesterDescriptor(XML.SAML11_PROTOCOL_ENUM);
101                 RoleDescriptor sp_role = provider.getSPSSODescriptor(XML.SAML11_PROTOCOL_ENUM);
102                 if (ar_role == null && sp_role == null) {
103                         log.info("SPSSO and Stand-Alone Requester roles not found in metadata for provider: (" + assertedId + ").");
104                         return null;
105                 }
106
107                 // Make sure that the supplied credential is valid for the selected provider role.
108                 if ((ar_role != null && support.getTrust().validate(chain[0], chain, ar_role))
109                                 || (sp_role != null && support.getTrust().validate(chain[0], chain, sp_role))) {
110                         log.info("Supplied credentials validated for this provider.");
111                         return assertedId;
112                 } else {
113                         log.error("Supplied credentials (" + chain[0].getSubjectX500Principal().getName(X500Principal.RFC2253)
114                                         + ") are NOT valid for provider (" + assertedId + ").");
115                         throw new InvalidProviderCredentialException("Invalid credentials.");
116                 }
117         }
118
119         /*
120          * @see edu.internet2.middleware.shibboleth.idp.ProtocolHandler#processRequest(javax.servlet.http.HttpServletRequest,
121          *      javax.servlet.http.HttpServletResponse, org.opensaml.SAMLRequest,
122          *      edu.internet2.middleware.shibboleth.idp.ProtocolSupport)
123          */
124         public SAMLResponse processRequest(HttpServletRequest request, HttpServletResponse response,
125                         SAMLRequest samlRequest, IdPProtocolSupport support) throws SAMLException, IOException, ServletException {
126
127                 if (samlRequest == null || samlRequest.getQuery() == null
128                                 || !(samlRequest.getQuery() instanceof SAMLAttributeQuery)) {
129                         log.error("Protocol Handler can only respond to SAML Attribute Queries.");
130                         throw new SAMLException("General error processing request.");
131                 }
132
133                 RelyingParty relyingParty = null;
134                 SAMLAttributeQuery attributeQuery = (SAMLAttributeQuery) samlRequest.getQuery();
135
136                 // This is the requester name that will be passed to subsystems
137                 String effectiveName = null;
138
139                 // Log the physical credential supplied, if any.
140                 X509Certificate[] credentials = (X509Certificate[]) request
141                                 .getAttribute("javax.servlet.request.X509Certificate");
142                 if (credentials == null || credentials.length == 0
143                                 || credentials[0].getSubjectX500Principal().getName(X500Principal.RFC2253).equals("")) {
144                         log.info("Request contained no credentials, treating as an unauthenticated service provider.");
145                 } else {
146                         log.info("Request contains credentials: ("
147                                         + credentials[0].getSubjectX500Principal().getName(X500Principal.RFC2253) + ").");
148
149                         // Try and authenticate the requester as any of the potentially relevant identifiers we know.
150                         try {
151                                 if (attributeQuery.getResource() != null) {
152                                         log.info("Remote provider has identified itself as: (" + attributeQuery.getResource() + ").");
153                                         effectiveName = authenticateAs(attributeQuery.getResource(), credentials, support);
154                                 }
155
156                                 if (effectiveName == null) {
157                                         log
158                                                         .info("Remote provider not yet identified, attempting to derive requesting provider from credentials.");
159
160                                         // Try the additional candidates.
161                                         String[] candidateNames = getCredentialNames(credentials[0]);
162                                         for (int c = 0; effectiveName == null && c < candidateNames.length; c++) {
163                                                 effectiveName = authenticateAs(candidateNames[c], credentials, support);
164                                         }
165                                 }
166                         } catch (InvalidProviderCredentialException ipc) {
167                                 throw new SAMLException(SAMLException.REQUESTER, "Invalid credentials for request.");
168                         }
169                 }
170
171                 if (effectiveName == null) {
172                         log.info("Unable to locate metadata about provider, treating as an unauthenticated service provider.");
173                         relyingParty = support.getServiceProviderMapper().getRelyingParty(null);
174                         if (log.isDebugEnabled()) {
175                                 log.debug("Using default Relying Party, " + relyingParty.getName() + " for unauthenticated provider.");
176                         }
177                 } else {
178                         // Identify a Relying Party
179                         log.debug("Mapping authenticated provider (" + effectiveName + ") to Relying Party.");
180                         relyingParty = support.getServiceProviderMapper().getRelyingParty(effectiveName);
181                 }
182
183                 // Fail if we can't honor SAML Subject Confirmation unless the only one supplied is
184                 // bearer, in which case this is probably a Shib 1.1 query, and we'll let it slide for now.
185                 // TODO: remove the compatibility with 1.1 and be strict about this?
186                 boolean hasConfirmationMethod = false;
187                 boolean hasOnlyBearer = true;
188                 Iterator iterator = attributeQuery.getSubject().getConfirmationMethods();
189                 while (iterator.hasNext()) {
190                         String method = (String) iterator.next();
191                         log.info("Request contains SAML Subject Confirmation method: (" + method + ").");
192                         hasConfirmationMethod = true;
193                         if (!method.equals(SAMLSubject.CONF_BEARER)) hasOnlyBearer = false;
194                 }
195                 if (hasConfirmationMethod && !hasOnlyBearer) { throw new SAMLException(SAMLException.REQUESTER,
196                                 "This SAML authority cannot honor requests containing the supplied SAML Subject Confirmation Method(s)."); }
197
198                 // Map Subject to local principal
199                 Principal principal = null;
200                 try {
201                         SAMLNameIdentifier nameId = attributeQuery.getSubject().getNameIdentifier();
202                         log.debug("Name Identifier format: (" + nameId.getFormat() + ").");
203                         NameIdentifierMapping mapping = null;
204                         try {
205                                 mapping = support.getNameMapper().getNameIdentifierMapping(new URI(nameId.getFormat()));
206                         } catch (URISyntaxException e) {
207                                 log.error("Invalid Name Identifier format.");
208                         }
209                         if (mapping == null) { throw new NameIdentifierMappingException("Name Identifier format not registered."); }
210
211                         // Don't honor the request if the active relying party configuration does not contain a mapping with the
212                         // name identifier format from the request
213                         if (!Arrays.asList(relyingParty.getNameMapperIds()).contains(mapping.getId())) { throw new NameIdentifierMappingException(
214                                         "Name Identifier format not valid for this relying party."); }
215
216                         principal = mapping.getPrincipal(nameId, relyingParty, relyingParty.getIdentityProvider());
217                         log.info("Request is for principal (" + principal.getName() + ").");
218
219                         // Get attributes from resolver
220                         Collection<? extends SAMLAttribute> attrs;
221                         Iterator requestedAttrsIterator = attributeQuery.getDesignators();
222                         if (requestedAttrsIterator.hasNext()) {
223                                 log.info("Request designates specific attributes, resolving this set.");
224                                 ArrayList<URI> requestedAttrs = new ArrayList<URI>();
225                                 while (requestedAttrsIterator.hasNext()) {
226                                         SAMLAttributeDesignator attribute = (SAMLAttributeDesignator) requestedAttrsIterator.next();
227                                         try {
228                                                 log.debug("Designated attribute: (" + attribute.getName() + ")");
229                                                 requestedAttrs.add(new URI(attribute.getName()));
230                                         } catch (URISyntaxException use) {
231                                                 log.error("Request designated an attribute name that does not conform "
232                                                                 + "to the required URI syntax (" + attribute.getName() + ").  Ignoring this attribute");
233                                         }
234                                 }
235
236                                 attrs = support.getReleaseAttributes(principal, relyingParty, effectiveName, requestedAttrs);
237                         } else {
238                                 log.info("Request does not designate specific attributes, resolving all available.");
239                                 attrs = support.getReleaseAttributes(principal, relyingParty, effectiveName);
240                         }
241
242                         log.info("Found " + attrs.size() + " attribute(s) for " + principal.getName());
243
244                         // Put attributes names in the transaction log when it is set to DEBUG
245                         if (support.getTransactionLog().isDebugEnabled() && attrs.size() > 0) {
246                                 StringBuffer attrNameBuffer = new StringBuffer();
247                                 for (SAMLAttribute attr : attrs) {
248                                         attrNameBuffer.append("(" + attr.getName() + ")");
249                                 }
250                                 support.getTransactionLog()
251                                                 .debug(
252                                                                 "Attribute assertion generated for provider (" + effectiveName
253                                                                                 + ") on behalf of principal (" + principal.getName()
254                                                                                 + ") with the following attributes: " + attrNameBuffer.toString());
255                         }
256
257                         SAMLResponse samlResponse = null;
258
259                         if (attrs == null || attrs.size() == 0) {
260                                 // No attribute found
261                                 samlResponse = new SAMLResponse(samlRequest.getId(), null, null, null);
262
263                         } else {
264                                 // Reference requested subject
265                                 SAMLSubject rSubject = (SAMLSubject) attributeQuery.getSubject().clone();
266
267                                 ArrayList<String> audiences = new ArrayList<String>();
268                                 if (relyingParty.getProviderId() != null) {
269                                         audiences.add(relyingParty.getProviderId());
270                                 }
271                                 if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
272                                         audiences.add(relyingParty.getName());
273                                 }
274
275                                 SAMLCondition condition = new SAMLAudienceRestrictionCondition(audiences);
276
277                                 // Put all attributes into an assertion
278                                 SAMLStatement statement = new SAMLAttributeStatement(rSubject, attrs);
279
280                                 // Set assertion expiration to longest attribute expiration
281                                 long max = 0;
282                                 for (SAMLAttribute attr : attrs) {
283                                         if (max < attr.getLifetime()) {
284                                                 max = attr.getLifetime();
285                                         }
286                                 }
287                                 Date now = new Date();
288                                 Date then = new Date(now.getTime() + (max * 1000)); // max is in
289                                 // seconds
290
291                                 SAMLAssertion sAssertion = new SAMLAssertion(relyingParty.getIdentityProvider().getProviderId(), now,
292                                                 then, Collections.singleton(condition), null, Collections.singleton(statement));
293
294                                 // Sign the assertions, if necessary
295                                 boolean metaDataIndicatesSignAssertions = false;
296                                 EntityDescriptor descriptor = support.lookup(relyingParty.getProviderId());
297                                 if (descriptor != null) {
298                                         AttributeRequesterDescriptor ar = descriptor
299                                                         .getAttributeRequesterDescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
300                                         if (ar != null) {
301                                                 if (ar.getWantAssertionsSigned()) {
302                                                         metaDataIndicatesSignAssertions = true;
303                                                 }
304                                         }
305                                         if (!metaDataIndicatesSignAssertions) {
306                                                 SPSSODescriptor sp = descriptor.getSPSSODescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
307                                                 if (sp != null) {
308                                                         if (sp.getWantAssertionsSigned()) {
309                                                                 metaDataIndicatesSignAssertions = true;
310                                                         }
311                                                 }
312                                         }
313                                 }
314                                 if (relyingParty.wantsAssertionsSigned() || metaDataIndicatesSignAssertions) {
315                                         support.signAssertions(new SAMLAssertion[]{sAssertion}, relyingParty);
316                                 }
317
318                                 samlResponse = new SAMLResponse(samlRequest.getId(), null, Collections.singleton(sAssertion), null);
319                         }
320
321                         if (log.isDebugEnabled()) { // This takes some processing, so only do it if we need to
322                                 log.debug("Dumping generated SAML Response:" + System.getProperty("line.separator")
323                                                 + samlResponse.toString());
324                         }
325
326                         log.info("Successfully created response for principal (" + principal.getName() + ").");
327
328                         if (effectiveName == null) {
329                                 support.getTransactionLog().info(
330                                                 "Attribute assertion issued to anonymous provider at (" + request.getRemoteAddr()
331                                                                 + ") on behalf of principal (" + principal.getName() + ").");
332                         } else {
333                                 support.getTransactionLog().info(
334                                                 "Attribute assertion issued to provider (" + effectiveName + ") on behalf of principal ("
335                                                                 + principal.getName() + ").");
336                         }
337
338                         return samlResponse;
339
340                 } catch (SAMLException e) {
341                         if (relyingParty.passThruErrors()) {
342                                 throw new SAMLException("General error processing request.", e);
343                         } else {
344                                 throw new SAMLException("General error processing request.");
345                         }
346
347                 } catch (InvalidNameIdentifierException e) {
348                         log.error("Could not associate the request's subject with a principal: " + e);
349                         if (relyingParty.passThruErrors()) {
350                                 throw new SAMLException(Arrays.asList(e.getSAMLErrorCodes()), "The supplied Subject was unrecognized.",
351                                                 e);
352                         } else {
353                                 throw new SAMLException(Arrays.asList(e.getSAMLErrorCodes()), "The supplied Subject was unrecognized.");
354                         }
355
356                 } catch (NameIdentifierMappingException e) {
357                         log.error("Encountered an error while mapping the name identifier from the request: " + e);
358                         if (relyingParty.passThruErrors()) {
359                                 throw new SAMLException("General error processing request.", e);
360                         } else {
361                                 throw new SAMLException("General error processing request.");
362                         }
363
364                 } catch (AAException e) {
365                         log.error("Encountered an error while resolving resolving attributes: " + e);
366                         if (relyingParty.passThruErrors()) {
367                                 throw new SAMLException("General error processing request.", e);
368                         } else {
369                                 throw new SAMLException("General error processing request.");
370                         }
371
372                 } catch (CloneNotSupportedException e) {
373                         log.error("Encountered an error while cloning request subject for use in response: " + e);
374                         if (relyingParty.passThruErrors()) {
375                                 throw new SAMLException("General error processing request.", e);
376                         } else {
377                                 throw new SAMLException("General error processing request.");
378                         }
379                 }
380         }
381 }