Fixed minor bugs introduced with recent ARP Engine refactorings. Thanks to Will...
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / idp / provider / ADFS_SSOHandler.java
1 /*
2  * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.] Licensed under the Apache License,
3  * Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy
4  * of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in
5  * writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
6  * OF ANY KIND, either express or implied. See the License for the specific language governing permissions and
7  * limitations under the License.
8  */
9
10 package edu.internet2.middleware.shibboleth.idp.provider;
11
12 import java.io.IOException;
13 import java.util.ArrayList;
14 import java.util.Arrays;
15 import java.util.Collection;
16 import java.util.Date;
17 import java.util.Iterator;
18 import java.util.Vector;
19
20 import javax.servlet.RequestDispatcher;
21 import javax.servlet.ServletException;
22 import javax.servlet.http.HttpServletRequest;
23 import javax.servlet.http.HttpServletResponse;
24
25 import org.apache.commons.codec.binary.Base64;
26 import org.apache.log4j.Logger;
27 import org.apache.xml.security.c14n.CanonicalizationException;
28 import org.apache.xml.security.c14n.Canonicalizer;
29 import org.apache.xml.security.c14n.InvalidCanonicalizerException;
30 import org.opensaml.SAMLAssertion;
31 import org.opensaml.SAMLAttribute;
32 import org.opensaml.SAMLAttributeStatement;
33 import org.opensaml.SAMLAudienceRestrictionCondition;
34 import org.opensaml.SAMLAuthenticationStatement;
35 import org.opensaml.SAMLCondition;
36 import org.opensaml.SAMLConfig;
37 import org.opensaml.SAMLException;
38 import org.opensaml.SAMLNameIdentifier;
39 import org.opensaml.SAMLRequest;
40 import org.opensaml.SAMLResponse;
41 import org.opensaml.SAMLStatement;
42 import org.opensaml.SAMLSubject;
43 import org.opensaml.SAMLSubjectStatement;
44 import org.opensaml.XML;
45 import org.w3c.dom.Document;
46 import org.w3c.dom.Element;
47
48 import edu.internet2.middleware.shibboleth.aa.AAException;
49 import edu.internet2.middleware.shibboleth.common.LocalPrincipal;
50 import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException;
51 import edu.internet2.middleware.shibboleth.common.RelyingParty;
52 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
53 import edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler;
54 import edu.internet2.middleware.shibboleth.idp.IdPProtocolSupport;
55 import edu.internet2.middleware.shibboleth.idp.InvalidClientDataException;
56 import edu.internet2.middleware.shibboleth.metadata.Endpoint;
57 import edu.internet2.middleware.shibboleth.metadata.EntityDescriptor;
58 import edu.internet2.middleware.shibboleth.metadata.SPSSODescriptor;
59
60 /**
61  * <code>ProtocolHandler</code> implementation that responds to ADFS SSO flows as specified in "WS-Federation: Passive
62  * Requestor Interoperability Profiles".
63  * 
64  * @author Walter Hoehn
65  */
66 public class ADFS_SSOHandler extends SSOHandler implements IdPProtocolHandler {
67
68         private static Logger log = Logger.getLogger(ADFS_SSOHandler.class.getName());
69         private static final String WA = "wsignin1.0";
70         private static final String WS_FED_PROTOCOL_ENUM = "http://schemas.xmlsoap.org/ws/2003/07/secext";
71         private static final Collection SUPPORTED_IDENTIFIER_FORMATS = Arrays.asList(new String[]{
72                         "urn:oasis:names:tc:SAML:1.1nameid-format:emailAddress", "http://schemas.xmlsoap.org/claims/UPN",
73                         "http://schemas.xmlsoap.org/claims/CommonName"});
74         private static final String CLAIMS_URI = "http://schemas.xmlsoap.org/claims";
75
76         /**
77          * Required DOM-based constructor.
78          */
79         public ADFS_SSOHandler(Element config) throws ShibbolethConfigurationException {
80
81                 super(config);
82         }
83
84         /*
85          * @see edu.internet2.middleware.shibboleth.idp.IdPResponder.ProtocolHandler#processRequest(javax.servlet.http.HttpServletRequest,
86          *      javax.servlet.http.HttpServletResponse)
87          */
88         public SAMLResponse processRequest(HttpServletRequest request, HttpServletResponse response,
89                         SAMLRequest samlRequest, IdPProtocolSupport support) throws SAMLException, ServletException, IOException {
90
91                 if (request == null) {
92                         log.error("Protocol Handler received a SAML Request, but is unable to handle it.");
93                         throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
94                 }
95
96                 // Set attributes that are needed by the jsp
97                 // ADFS spec says always send (wa)
98                 request.setAttribute("wa", ADFS_SSOHandler.WA);
99                 // Passthru (wctx) if we get one
100                 if (request.getParameter("wctx") != null && !request.getParameter("wctx").equals("")) {
101                         request.setAttribute("wctx", request.getParameter("wctx"));
102                 }
103
104                 try {
105                         // Ensure that we have the required data from the servlet container
106                         validateEngineData(request);
107                         validateAdfsSpecificData(request);
108
109                         // Get the authN info
110                         String username = support.getIdPConfig().getAuthHeaderName().equalsIgnoreCase("REMOTE_USER") ? request
111                                         .getRemoteUser() : request.getHeader(support.getIdPConfig().getAuthHeaderName());
112                         if ((username == null) || (username.equals(""))) { throw new InvalidClientDataException(
113                                         "Unauthenticated principal. This protocol handler requires that authentication information be "
114                                                         + "provided from the servlet container."); }
115                         LocalPrincipal principal = new LocalPrincipal(username);
116
117                         // Select the appropriate Relying Party configuration for the request
118                         String remoteProviderId = request.getParameter("wtrealm");
119                         log.debug("Remote provider has identified itself as: (" + remoteProviderId + ").");
120                         RelyingParty relyingParty = support.getServiceProviderMapper().getRelyingParty(remoteProviderId);
121
122                         // Grab the metadata for the provider
123                         EntityDescriptor descriptor = support.lookup(relyingParty.getProviderId());
124                         if (descriptor == null) {
125                                 log.info("No metadata found for provider: (" + relyingParty.getProviderId() + ").");
126                                 throw new InvalidClientDataException(
127                                                 "The specified Service Provider is unkown to this Identity Provider.");
128                         }
129
130                         // Make sure we have proper WS-Fed metadata
131                         SPSSODescriptor sp = descriptor.getSPSSODescriptor(ADFS_SSOHandler.WS_FED_PROTOCOL_ENUM);
132                         if (sp == null) {
133                                 log.info("Inappropriate metadata for provider: no WS-Federation binding.");
134                                 throw new InvalidClientDataException(
135                                                 "Unable to communicate with the specified Service Provider via this protocol.");
136                         }
137
138                         // If an acceptance URL was supplied, validate it
139                         String acceptanceURL = request.getParameter("wreply");
140                         if (acceptanceURL != null && !acceptanceURL.equals("")) {
141                                 if (isValidAssertionConsumerURL(sp, acceptanceURL)) {
142                                         log.info("Supplied consumer URL validated for this provider.");
143                                 } else {
144                                         log.error("Assertion consumer service URL (" + acceptanceURL + ") is NOT valid for provider ("
145                                                         + relyingParty.getProviderId() + ").");
146                                         throw new InvalidClientDataException("Invalid assertion consumer service URL.");
147                                 }
148                                 // if none was supplied, pull one from the metadata
149                         } else {
150                                 Endpoint endpoint = sp.getAssertionConsumerServiceManager().getEndpointByBinding(
151                                                 ADFS_SSOHandler.WS_FED_PROTOCOL_ENUM);
152                                 if (endpoint == null || endpoint.getLocation() == null) {
153                                         log.error("No Assertion consumer service URL is available for provider ("
154                                                         + relyingParty.getProviderId() + ") via request the SSO request or the metadata.");
155                                         throw new InvalidClientDataException("Unable to determine assertion consumer service URL.");
156                                 }
157                                 acceptanceURL = endpoint.getLocation();
158                         }
159                         // Needed for the form
160                         request.setAttribute("wreply", acceptanceURL);
161
162                         // Create SAML Name Identifier & Subject
163                         SAMLNameIdentifier nameId;
164                         try {
165                                 nameId = getNameIdentifier(support.getNameMapper(), principal, relyingParty, descriptor);
166                                 // ADFS spec limits which name identifier formats can be used
167                                 if (!ADFS_SSOHandler.SUPPORTED_IDENTIFIER_FORMATS.contains(nameId.getFormat())) {
168                                         log.error("SAML Name Identifier format (" + nameId.getFormat()
169                                                         + ") is inappropriate for use with ADFS provider.");
170                                         throw new SAMLException(
171                                                         "Error converting principal to SAML Name Identifier: Invalid ADFS Name Identifier format.");
172                                 }
173
174                         } catch (NameIdentifierMappingException e) {
175                                 log.error("Error converting principal to SAML Name Identifier: " + e);
176                                 throw new SAMLException("Error converting principal to SAML Name Identifier.", e);
177                         }
178
179                         // ADFS profile requires an authentication method
180                         String authenticationMethod = request.getHeader("SAMLAuthenticationMethod");
181                         if (authenticationMethod == null || authenticationMethod.equals("")) {
182                                 authenticationMethod = relyingParty.getDefaultAuthMethod().toString();
183                                 log.debug("User was authenticated via the default method for this relying party ("
184                                                 + authenticationMethod + ").");
185                         } else {
186                                 log.debug("User was authenticated via the method (" + authenticationMethod + ").");
187                         }
188
189                         SAMLSubject authNSubject = new SAMLSubject(nameId, null, null, null);
190
191                         // We always do POST with ADFS
192                         respondWithPOST(request, response, support, principal, relyingParty, descriptor, acceptanceURL, nameId,
193                                         authenticationMethod, authNSubject);
194
195                 } catch (InvalidClientDataException e) {
196                         throw new SAMLException(SAMLException.RESPONDER, e.getMessage());
197                 } catch (SecurityTokenResponseException e) {
198                         throw new SAMLException(SAMLException.RESPONDER, e.getMessage());
199                 }
200                 return null;
201         }
202
203         private void respondWithPOST(HttpServletRequest request, HttpServletResponse response, IdPProtocolSupport support,
204                         LocalPrincipal principal, RelyingParty relyingParty, EntityDescriptor descriptor, String acceptanceURL,
205                         SAMLNameIdentifier nameId, String authenticationMethod, SAMLSubject authNSubject) throws SAMLException,
206                         IOException, ServletException, SecurityTokenResponseException {
207
208                 // We should always send a single token (SAML assertion)
209                 SAMLAssertion assertion = generateAssertion(request, relyingParty, descriptor, nameId, authenticationMethod,
210                                 getAuthNTime(request), authNSubject);
211
212                 generateAttributes(support, principal, relyingParty, assertion, request);
213
214                 // ADFS spec says assertions should always be signed
215                 support.signAssertions((SAMLAssertion[]) new SAMLAssertion[]{assertion}, relyingParty);
216
217                 // Wrap assertion in security token response and create form
218                 createPOSTForm(request, response, new SecurityTokenResponse(assertion, relyingParty.getProviderId()));
219
220                 // Make transaction log entry
221                 support.getTransactionLog().info(
222                                 "ADFS security token issued to provider (" + relyingParty.getProviderId()
223                                                 + ") on behalf of principal (" + principal.getName() + ").");
224         }
225
226         private void generateAttributes(IdPProtocolSupport support, LocalPrincipal principal, RelyingParty relyingParty,
227                         SAMLAssertion assertion, HttpServletRequest request) throws SAMLException {
228
229                 try {
230                         Collection<? extends SAMLAttribute> attributes = support.getReleaseAttributes(principal, relyingParty,
231                                         relyingParty.getProviderId());
232                         log.info("Found " + attributes.size() + " attribute(s) for " + principal.getName());
233
234                         // Bail if we didn't get any attributes
235                         if (attributes == null || attributes.size() < 1) {
236                                 log.info("No attributes resolved.");
237                                 return;
238                         }
239
240                         // The ADFS spec recommends that all attributes have this URI, but it doesn't require it
241                         for (SAMLAttribute attribute : attributes) {
242                                 if (!attribute.getNamespace().equals(CLAIMS_URI)) {
243                                         log.warn("It is recommended that all attributes sent via the ADFS SSO handler "
244                                                         + "have a namespace of (" + CLAIMS_URI + ").  The attribute (" + attribute.getName()
245                                                         + ") has a namespace of (" + attribute.getNamespace() + ").");
246                                 }
247                         }
248
249                         // Reference requested subject
250                         SAMLSubject attrSubject = (SAMLSubject) ((SAMLSubjectStatement) assertion.getStatements().next())
251                                         .getSubject().clone();
252
253                         // ADFS spec says to include authN and attribute statements in the same assertion
254                         log.debug("Merging attributes into existing authn assertion");
255                         assertion.addStatement(new SAMLAttributeStatement(attrSubject, Arrays.asList(attributes)));
256
257                         if (log.isDebugEnabled()) {
258                                 log.debug("Dumping combined Assertion:" + System.getProperty("line.separator") + assertion.toString());
259                         }
260
261                 } catch (AAException e) {
262                         log.error("An error was encountered while generating assertion for attribute push: " + e);
263                         throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
264                 } catch (CloneNotSupportedException e) {
265                         log.error("An error was encountered while generating assertion for attribute push: " + e);
266                         throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
267                 }
268         }
269
270         private SAMLAssertion generateAssertion(HttpServletRequest request, RelyingParty relyingParty,
271                         EntityDescriptor descriptor, SAMLNameIdentifier nameId, String authenticationMethod, Date authTime,
272                         SAMLSubject subject) throws SAMLException, IOException {
273
274                 // Bearer method is recommended by the ADFS spec
275                 subject.addConfirmationMethod(SAMLSubject.CONF_BEARER);
276
277                 // ADFS spec requires a single audience of the SP
278                 ArrayList<String> audiences = new ArrayList<String>();
279                 if (relyingParty.getProviderId() != null) {
280                         audiences.add(relyingParty.getProviderId());
281                 }
282                 Vector<SAMLCondition> conditions = new Vector<SAMLCondition>(1);
283                 if (audiences != null && audiences.size() > 0) conditions.add(new SAMLAudienceRestrictionCondition(audiences));
284
285                 // Determine the correct issuer
286                 String issuer = relyingParty.getIdentityProvider().getProviderId();
287
288                 // Create the assertion
289                 // NOTE the ADFS spec says not to specify a locality
290                 SAMLStatement[] statements = {new SAMLAuthenticationStatement(subject, authenticationMethod, authTime, null,
291                                 null, null)};
292
293                 // Package attributes
294                 log.info("Resolving attributes.");
295
296                 SAMLAssertion assertion = new SAMLAssertion(issuer, new Date(System.currentTimeMillis()), new Date(System
297                                 .currentTimeMillis() + 300000), conditions, null, Arrays.asList(statements));
298
299                 if (log.isDebugEnabled()) {
300                         log.debug("Dumping generated Assertion:" + System.getProperty("line.separator") + assertion.toString());
301                 }
302
303                 return assertion;
304         }
305
306         /*
307          * @see edu.internet2.middleware.shibboleth.idp.IdPResponder.ProtocolHandler#getHandlerName()
308          */
309         public String getHandlerName() {
310
311                 return "ADFS SSO Handler";
312         }
313
314         private void validateAdfsSpecificData(HttpServletRequest request) throws InvalidClientDataException {
315
316                 // Required by spec, must have the constant value
317                 if (request.getParameter("wa") == null || !request.getParameter("wa").equals(ADFS_SSOHandler.WA)) { throw new InvalidClientDataException(
318                                 "Invalid data from Service Provider: missing or invalid (wa) parameter."); }
319
320                 // Required by spec
321                 if ((request.getParameter("wtrealm") == null) || (request.getParameter("wtrealm").equals(""))) { throw new InvalidClientDataException(
322                                 "Invalid data from Service Provider:missing or invalid (wtrealm) parameter."); }
323         }
324
325         private static void createPOSTForm(HttpServletRequest req, HttpServletResponse res,
326                         SecurityTokenResponse tokenResponse) throws IOException, ServletException, SecurityTokenResponseException {
327
328                 req.setAttribute("wresult", tokenResponse.toXmlString());
329
330                 if (log.isDebugEnabled()) {
331                         log.debug("Dumping generated Security Token Response:" + System.getProperty("line.separator")
332                                         + tokenResponse.toXmlString());
333                 }
334
335                 RequestDispatcher rd = req.getRequestDispatcher("/adfs.jsp");
336                 rd.forward(req, res);
337         }
338
339         /**
340          * Boolean indication of whethere or not a given assertion consumer URL is valid for a given SP.
341          */
342         private static boolean isValidAssertionConsumerURL(SPSSODescriptor descriptor, String shireURL)
343                         throws InvalidClientDataException {
344
345                 Iterator endpoints = descriptor.getAssertionConsumerServiceManager().getEndpoints();
346                 while (endpoints.hasNext()) {
347                         if (shireURL.equals(((Endpoint) endpoints.next()).getLocation())) { return true; }
348                 }
349                 log.info("Supplied consumer URL not found in metadata.");
350                 return false;
351         }
352
353 }
354
355 class SecurityTokenResponse {
356
357         private static Logger log = Logger.getLogger(SecurityTokenResponse.class.getName());
358         private static SAMLConfig config = SAMLConfig.instance();
359         private static String WS_TRUST_SCHEMA = "http://schemas.xmlsoap.org/ws/2005/02/trust";
360         private static String WS_POLICY_SCHEMA = "http://schemas.xmlsoap.org/ws/2004/09/policy";
361         private static String WS_ADDRESSING_SCHEMA = "http://schemas.xmlsoap.org/ws/2004/08/addressing";
362         private Document response;
363
364         SecurityTokenResponse(SAMLAssertion assertion, String remoteProviderId) throws SecurityTokenResponseException,
365                         SAMLException {
366
367                 response = XML.parserPool.newDocument();
368
369                 // Create root response element
370                 Element root = response.createElementNS(WS_TRUST_SCHEMA, "RequestSecurityTokenResponse");
371                 root.setAttributeNS(XML.XMLNS_NS, "xmlns", WS_TRUST_SCHEMA);
372                 response.appendChild(root);
373
374                 // Tie to remote endpoint
375                 Element appliesTo = response.createElementNS(WS_POLICY_SCHEMA, "AppliesTo");
376                 appliesTo.setAttributeNS(XML.XMLNS_NS, "xmlns", WS_POLICY_SCHEMA);
377                 root.appendChild(appliesTo);
378                 Element endpointRef = response.createElementNS(WS_ADDRESSING_SCHEMA, "EndpointReference");
379                 endpointRef.setAttributeNS(XML.XMLNS_NS, "xmlns", WS_ADDRESSING_SCHEMA);
380                 appliesTo.appendChild(endpointRef);
381                 Element address = response.createElementNS(WS_ADDRESSING_SCHEMA, "Address");
382                 address.appendChild(response.createTextNode(remoteProviderId));
383                 endpointRef.appendChild(address);
384
385                 // Add security token
386                 Element token = response.createElementNS(WS_TRUST_SCHEMA, "RequestedSecurityToken");
387
388                 token.appendChild(assertion.toDOM(response));
389                 root.appendChild(token);
390
391         }
392
393         public byte[] toBase64() throws SecurityTokenResponseException {
394
395                 try {
396                         Canonicalizer canonicalizier = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
397                         byte[] canonicalized = canonicalizier.canonicalizeSubtree(response, config
398                                         .getProperty("org.opensaml.inclusive-namespace-prefixes"));
399
400                         return Base64.encodeBase64Chunked(canonicalized);
401                 } catch (InvalidCanonicalizerException e) {
402                         log.error("Error Canonicalizing Security Token Response: " + e);
403                         throw new SecurityTokenResponseException(e.getMessage());
404                 }
405
406                 catch (CanonicalizationException e) {
407                         log.error("Error Canonicalizing Security Token Response: " + e);
408                         throw new SecurityTokenResponseException(e.getMessage());
409                 }
410         }
411
412         public String toXmlString() throws SecurityTokenResponseException {
413
414                 try {
415                         Canonicalizer canonicalizier = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
416                         byte[] canonicalized = canonicalizier.canonicalizeSubtree(response, config
417                                         .getProperty("org.opensaml.inclusive-namespace-prefixes"));
418                         return new String(canonicalized);
419
420                 } catch (InvalidCanonicalizerException e) {
421                         log.error("Error Canonicalizing Security Token Response: " + e);
422                         throw new SecurityTokenResponseException(e.getMessage());
423                 }
424
425                 catch (CanonicalizationException e) {
426                         log.error("Error Canonicalizing Security Token Response: " + e);
427                         throw new SecurityTokenResponseException(e.getMessage());
428                 }
429         }
430
431 }
432
433 class SecurityTokenResponseException extends Exception {
434
435         SecurityTokenResponseException(String message) {
436
437                 super(message);
438         }
439 }