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