Support attribute Push
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / serviceprovider / AttributeRequestor.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 /*
18  * AttributeRequestor.java
19  * 
20  * Generate a SAMLRequest to the AA for Attributes, then process the
21  * reply. In C++, this logic was InternalCCacheEntry::getNewResponse() 
22  * in shib-ccache.cpp.
23  * 
24  * The User Authentication was previously processed by ShibPOSTProfile
25  * and was stored by SessionManager in a Session object. Although the
26  * attributes might be fetched immediately, that is a strategy issue.
27  * In Java, we isolate the Attribute fetch in this separate module.
28  * 
29  * The Session block retains a copy of the original SAMLStatement from
30  * the POST, and it contains information about the remote Entity.
31  * However, the POST came from the HS (or IIDPProvider if you want to
32  * use SAML 2 terms), and this transaction has to go to the AA. So
33  * Metadata must be used to obtain the AttributeAuthorityRole and its
34  * associated Endpoint (URL).
35  * 
36  * The ApplicationInfo object for the configured Application presents
37  * a getAttributeDesignators method that can return a list of attributes
38  * to specify in the request. However, I can find no configuration element
39  * that corresponds to this, and no example logic. For now that method
40  * returns an empty collection and no particular attributes are requested.
41  * 
42  * The actual SSL session and exchange of data is performed by OpenSAML.
43  * Our interface to SAML is through the separate ShibBind module. The
44  * layers of processing and responsibilites need to be understood.
45  * 
46  * ShibBind uses the Metadata for the User's ID Providing Entity to
47  * locate the AttributeAuthorityRole and therefore the AA URL. This
48  * is passed to OpenSAML along with the request. Upon return, if any
49  * statements are signed it is the responsiblilty of ShibBind to call
50  * the Trust implementations to validate the signatures.
51  * 
52  * This module then checks, by calling the isSigned() property of
53  * SAMLObjects, to make sure that everything that is supposed to be
54  * signed actually was signed. ShibBind knows if a signature is valid,
55  * but this module knows if a signature was requred. This module also
56  * applies AAP to examine attributes and values and discard those that
57  * the policy doesn't accept.
58  * 
59  * Recovery Context: All exceptions handled and logged internally.
60  */
61 package edu.internet2.middleware.shibboleth.serviceprovider;
62
63 import java.security.Key;
64 import java.security.cert.X509Certificate;
65 import java.util.ArrayList;
66 import java.util.Arrays;
67 import java.util.Collection;
68 import java.util.Iterator;
69
70 import org.apache.log4j.Logger;
71 import org.apache.xml.security.signature.XMLSignature;
72 import org.opensaml.SAMLAssertion;
73 import org.opensaml.SAMLAttributeQuery;
74 import org.opensaml.SAMLAuthenticationStatement;
75 import org.opensaml.SAMLException;
76 import org.opensaml.SAMLRequest;
77 import org.opensaml.SAMLResponse;
78 import org.opensaml.SAMLSubject;
79 import org.opensaml.XML;
80
81 import edu.internet2.middleware.shibboleth.common.Credential;
82 import edu.internet2.middleware.shibboleth.common.Credentials;
83 import edu.internet2.middleware.shibboleth.metadata.AttributeAuthorityDescriptor;
84 import edu.internet2.middleware.shibboleth.metadata.EntityDescriptor;
85 import edu.internet2.middleware.shibboleth.serviceprovider.ServiceProviderConfig.ApplicationInfo;
86
87 /**
88  * A static class with a static method. No objects are created
89  * 
90  * @author Howard Gilbert
91  */
92 public class AttributeRequestor {
93         
94         private static Logger log = Logger.getLogger(AttributeRequestor.class);
95         private static ServiceProviderContext context   = ServiceProviderContext.getInstance();
96         
97         private AttributeRequestor() {} // Prevent instantiation
98         
99         /**
100          * Request SAML Attribute response from AA
101          * 
102          * @param session Session object (from authentication POST)
103          * @return true if Attributes successfully stored in the Session
104          * @throws MetadataException If IdP has no configured AA
105          * @throws SAMLException If there is a problem with the reply
106          */
107         static 
108                         boolean    // return false if attributes are not fetched
109         fetchAttributes(
110                         Session session){
111                 
112             log.debug("Fetching attributes for session "+session.getKey()+
113                     " from "+session.getEntityId());
114             
115                 // Get local references to configuration objects
116                 ServiceProviderConfig config = context.getServiceProviderConfig();
117                 ApplicationInfo appinfo = config.getApplication(session.getApplicationId());
118                 
119         SAMLResponse response = session.getAttributeResponse();
120
121         // The Entity name was fed by by ShibPOSTProfile.accept(). Look it up in the
122         // Metadata now and return the Entity object.
123                 EntityDescriptor entity = appinfo.lookup(session.getEntityId());
124                 if (entity==null) {
125                         log.error("Entity(Site) deleted from Metadata since authentication POST received: "+session.getEntityId());
126                         return false;
127                 }
128         
129                 SAMLRequest request = null;
130                 
131         // Find the Shibboleth protocol AA Role configured in the Metadat for this Entity. 
132                 AttributeAuthorityDescriptor aa = 
133                     entity.getAttributeAuthorityDescriptor(XML.SAML11_PROTOCOL_ENUM); // throws MetadataException
134                 if (aa==null) {
135                     log.error("No Attribute Authority in Metadata for ID="+entity.getId());
136                     return false;
137                 }
138                 
139                 if (response==null) {
140                     // Build the Attribute Query 
141                     SAMLAttributeQuery query = null;
142                     SAMLSubject subject;
143                     try {
144                         // Get the POST data from the Session. It has the Subject and its source.
145                         SAMLAuthenticationStatement authenticationStatement = session.getAuthenticationStatement();
146                         if (authenticationStatement==null) {
147                             log.error("Session contains no Authentication Statement." );
148                             return false;
149                         }
150                         SAMLSubject subject2 = authenticationStatement.getSubject();
151                         if (subject2==null) {
152                             log.error("Session Authentication Statement contains no Subject." );
153                             return false;
154                         }
155                         subject = (SAMLSubject) subject2.clone();
156                     } catch (Exception e) {
157                         log.error("Unable to generate the query SAMLSubject from the Authenticaiton." );
158                         return false;
159                     }
160                     log.debug("Subject (Handle) is "+subject.getNameIdentifier());
161                     
162                     
163                     
164                     
165                     Collection attributeDesignators = appinfo.getAttributeDesignators();
166                     try {
167                         query = 
168                             new SAMLAttributeQuery(
169                                     subject,                     // Subject (i.e. Handle) from authentication
170                                     appinfo.getProviderId(),  // SP Entity name
171                                     attributeDesignators // Attributes to request, null for everything
172                             );
173                         
174                         // Wrap the Query in a request
175                         request = new SAMLRequest(query);
176                     } catch (SAMLException e) {
177                         log.error("AttributeRequestor unable to build SAML Query for Session "+session.getKey());
178                         return false;
179                     }
180                     
181                     String credentialId = appinfo.getCredentialIdForEntity(entity);
182                     if (credentialId!=null)
183                         possiblySignRequest(config.getCredentials(), request, credentialId);
184                     
185                     // ShibBinding will extract URLs from the Metadata and build
186                     // parameters so SAML can create the session. It also interfaces
187                     // to Trust to verify that any signed objects have trusted signatures.
188                     try {
189                         ShibBinding binding = new ShibBinding(session.getApplicationId());
190                         response = binding.send(request,aa,null,null,appinfo);
191                     } catch (SAMLException e) {;} // response will be null
192                     if (response==null) {
193                         log.error("AttributeRequestor Query to remote AA returned no response from "+session.getEntityId());
194                         return false;
195                     }
196                 } else {
197                     log.info("Bypassing Attribute Query because Attributes already Pushed.");
198         }
199                 
200                 // Check each assertion in the response.
201         int acount = 0;
202                 Iterator assertions = response.getAssertions();
203         ArrayList assertionList = new ArrayList();
204         while (assertions.hasNext()) {
205             assertionList.add(assertions.next());
206         }
207         assertions=assertionList.iterator();
208                 while (assertions.hasNext()) {
209                         SAMLAssertion assertion = (SAMLAssertion) assertions.next();
210 //                      if (signedAssertions && !assertion.isSigned()) {
211 //                              log.warn("AttributeRequestor has removed unsigned assertion from response from "+session.getEntityId());
212 //                              response.removeAssertion(acount);
213 //                              continue;
214 //                      }
215                         
216             try {
217                 appinfo.applyAAP(assertion,aa); // apply each AAP to this assertion
218                 acount++;
219             }
220                         catch (SAMLException ex) {
221                 response.removeAssertion(acount); // AAP rejected all statements for this assertion
222             }
223                 }
224
225                 // A response may end up with no attributes, but that is not an error.
226                 // Maybe there is just nothing important to say about this user.
227                 
228                 session.setAttributeResponse(response); // Save response in Session object
229                 return true;
230         }
231
232     /**
233      * Given a credentialId from the CredentialUse/RelyingParty stuff,
234      * find a corresponding Credential element and use its Key/Cert to 
235      * sign the Request. 
236      * 
237      * @oaran credentials Credentials object from config file
238      * @param request SAML AA Query request
239      * @param credentialId Siging Id from CredentialUse
240      */
241         static void possiblySignRequest(
242             Credentials credentials, 
243                 SAMLRequest request,
244                 String credentialId) {
245         
246         
247             if (credentials==null) {
248             log.error("No Credentials Element in SP Config file.");
249                 return;
250         }
251             Credential credential = credentials.getCredential(credentialId);
252             if (credential==null) {
253             log.error("No credential found for id "+credentialId);
254                 return;
255         }
256             Key key = credential.getPrivateKey();
257             X509Certificate[] certificateChain = credential.getX509CertificateChain();
258             try {
259                 request.sign(XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1,key,Arrays.asList(certificateChain));
260                 log.debug("Attribute Request signed with "+credentialId);
261             } catch (SAMLException e) {
262                 log.error("Unable to sign Attribute Request", e);
263             }
264         }
265
266 }