523e7ef15db2055ec2ec99a1bd3eec8b2f8acbab
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / idp / provider / ShibbolethV1SSOHandler.java
1 /*
2  * Created on Mar 10, 2005
3  *
4  * TODO To change the template for this generated file go to
5  * Window - Preferences - Java - Code Generation - Code and Comments
6  */
7 package edu.internet2.middleware.shibboleth.idp.provider;
8
9 import java.io.IOException;
10 import java.net.URLEncoder;
11 import java.util.ArrayList;
12 import java.util.Arrays;
13 import java.util.Date;
14 import java.util.Iterator;
15 import java.util.Vector;
16
17 import javax.servlet.RequestDispatcher;
18 import javax.servlet.ServletException;
19 import javax.servlet.http.HttpServletRequest;
20 import javax.servlet.http.HttpServletResponse;
21 import javax.xml.namespace.QName;
22
23 import org.apache.log4j.Logger;
24 import org.opensaml.SAMLAssertion;
25 import org.opensaml.SAMLAudienceRestrictionCondition;
26 import org.opensaml.SAMLAuthenticationStatement;
27 import org.opensaml.SAMLAuthorityBinding;
28 import org.opensaml.SAMLBinding;
29 import org.opensaml.SAMLBrowserProfile;
30 import org.opensaml.SAMLException;
31 import org.opensaml.SAMLNameIdentifier;
32 import org.opensaml.SAMLRequest;
33 import org.opensaml.SAMLResponse;
34 import org.opensaml.SAMLStatement;
35 import org.opensaml.SAMLSubject;
36 import org.w3c.dom.Document;
37
38 import sun.misc.BASE64Decoder;
39
40 import edu.internet2.middleware.shibboleth.common.AuthNPrincipal;
41 import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException;
42 import edu.internet2.middleware.shibboleth.common.RelyingParty;
43 import edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler;
44 import edu.internet2.middleware.shibboleth.idp.IdPProtocolSupport;
45 import edu.internet2.middleware.shibboleth.idp.InvalidClientDataException;
46
47 import edu.internet2.middleware.shibboleth.metadata.Endpoint;
48 import edu.internet2.middleware.shibboleth.metadata.EntityDescriptor;
49 import edu.internet2.middleware.shibboleth.metadata.SPSSODescriptor;
50
51
52 /**
53  * @author Walter Hoehn
54  */
55 public class ShibbolethV1SSOHandler implements IdPProtocolHandler {
56
57         private static Logger log = Logger.getLogger(ShibbolethV1SSOHandler.class.getName());
58
59         /*
60          * (non-Javadoc)
61          * 
62          * @see edu.internet2.middleware.shibboleth.idp.IdPResponder.ProtocolHandler#validForRequest(javax.servlet.http.HttpServletRequest)
63          */
64         // TODO move this into the process method
65         boolean validForRequest(HttpServletRequest request) {
66
67                 if (request.getParameter("target") != null && !request.getParameter("target").equals("")
68                                 && request.getParameter("shire") != null && !request.getParameter("shire").equals("")) {
69                         log.debug("Found (target) and (shire) parameters.  Request "
70                                         + "appears to be valid for the Shibboleth v1 profile.");
71                         return true;
72                 } else {
73                         return false;
74                 }
75         }
76
77         /*
78          * (non-Javadoc)
79          * 
80          * @see edu.internet2.middleware.shibboleth.idp.IdPResponder.ProtocolHandler#processRequest(javax.servlet.http.HttpServletRequest,
81          *      javax.servlet.http.HttpServletResponse)
82          */
83         public SAMLResponse processRequest(HttpServletRequest request, HttpServletResponse response,
84                         SAMLRequest samlRequest, IdPProtocolSupport support) throws InvalidClientDataException, SAMLException,
85                         ServletException, IOException {
86
87                 // TODO make sure the saml request is null for now
88
89                 // Ensure that we have the required data from the servlet container
90                 IdPProtocolSupport.validateEngineData(request);
91
92                 // Get the authN info
93                 String username = support.getIdPConfig().getAuthHeaderName().equalsIgnoreCase("REMOTE_USER") ? request
94                                 .getRemoteUser() : request.getHeader(support.getIdPConfig().getAuthHeaderName());
95
96                 // Select the appropriate Relying Party configuration for the request
97                 RelyingParty relyingParty = null;
98                 String remoteProviderId = request.getParameter("providerId");
99
100                 // If the target did not send a Provider Id, then assume it is a Shib
101                 // 1.1 or older target
102                 if (remoteProviderId == null) {
103                         relyingParty = support.getServiceProviderMapper().getLegacyRelyingParty();
104                 } else if (remoteProviderId.equals("")) {
105                         throw new InvalidClientDataException("Invalid service provider id.");
106                 } else {
107                         log.debug("Remote provider has identified itself as: (" + remoteProviderId + ").");
108                         relyingParty = support.getServiceProviderMapper().getRelyingParty(remoteProviderId);
109                 }
110
111                 // Grab the metadata for the provider
112                 EntityDescriptor provider = support.lookup(relyingParty.getProviderId());
113
114                 // Determine the acceptance URL
115                 String acceptanceURL = request.getParameter("shire");
116
117                 // Make sure that the selected relying party configuration is appropriate for this
118                 // acceptance URL
119                 if (!relyingParty.isLegacyProvider()) {
120
121                         if (provider == null) {
122                                 log.info("No metadata found for provider: (" + relyingParty.getProviderId() + ").");
123                                 relyingParty = support.getServiceProviderMapper().getRelyingParty(null);
124
125                         } else {
126
127                                 if (isValidAssertionConsumerURL(provider, acceptanceURL)) {
128                                         log.info("Supplied consumer URL validated for this provider.");
129                                 } else {
130                                         log.error("Assertion consumer service URL (" + acceptanceURL + ") is NOT valid for provider ("
131                                                         + relyingParty.getProviderId() + ").");
132                                         throw new InvalidClientDataException("Invalid assertion consumer service URL.");
133                                 }
134                         }
135                 }
136
137                 // Create SAML Name Identifier
138                 SAMLNameIdentifier nameId;
139                 try {
140                         nameId = support.getNameMapper().getNameIdentifierName(relyingParty.getHSNameFormatId(),
141                                         new AuthNPrincipal(username), relyingParty, relyingParty.getIdentityProvider());
142                 } catch (NameIdentifierMappingException e) {
143                         log.error("Error converting principal to SAML Name Identifier: " + e);
144                         throw new SAMLException("Error converting principal to SAML Name Identifier.", e);
145                 }
146
147                 String authenticationMethod = request.getHeader("SAMLAuthenticationMethod");
148                 if (authenticationMethod == null || authenticationMethod.equals("")) {
149                         authenticationMethod = relyingParty.getDefaultAuthMethod().toString();
150                         log.debug("User was authenticated via the default method for this relying party (" + authenticationMethod
151                                         + ").");
152                 } else {
153                         log.debug("User was authenticated via the method (" + authenticationMethod + ").");
154                 }
155
156                 // TODO change name!!!
157                 // TODO We might someday want to provide a mechanism for the authenticator to specify the auth time
158                 SAMLAssertion[] assertions = foo(request, relyingParty, provider, nameId, authenticationMethod, new Date(System
159                                 .currentTimeMillis()));
160
161                 // TODO do assertion signing for artifact stuff
162
163                 // SAML Artifact profile
164                 if (useArtifactProfile(provider, acceptanceURL)) {
165                         log.debug("Responding with Artifact profile.");
166
167                         // Create artifacts for each assertion
168                         ArrayList artifacts = new ArrayList();
169                         for (int i = 0; i < assertions.length; i++) {
170                                 // TODO replace the artifact stuff here!!!
171                                 // artifacts.add(artifactMapper.generateArtifact(assertions[i], relyingParty));
172                         }
173
174                         // Assemble the query string
175                         StringBuffer destination = new StringBuffer(acceptanceURL);
176                         destination.append("?TARGET=");
177
178                         destination.append(URLEncoder.encode(request.getParameter("target"), "UTF-8"));
179
180                         Iterator iterator = artifacts.iterator();
181                         StringBuffer artifactBuffer = new StringBuffer(); // Buffer for the transaction log
182                         while (iterator.hasNext()) {
183                                 destination.append("&SAMLart=");
184                                 String artifact = (String) iterator.next();
185
186                                 destination.append(URLEncoder.encode(artifact, "UTF-8"));
187                                 artifactBuffer.append("(" + artifact + ")");
188
189                         }
190                         log.debug("Redirecting to (" + destination.toString() + ").");
191                         response.sendRedirect(destination.toString()); // Redirect to the artifact receiver
192
193                         support.getTransactionLog().info(
194                                         "Assertion artifact(s) (" + artifactBuffer.toString() + ") issued to provider ("
195                                                         + relyingParty.getIdentityProvider().getProviderId() + ") on behalf of principal ("
196                                                         + username + "). Name Identifier: (" + nameId.getName() + "). Name Identifier Format: ("
197                                                         + nameId.getFormat() + ").");
198
199                         // SAML POST profile
200                 } else {
201                         log.debug("Responding with POST profile.");
202                         request.setAttribute("acceptanceURL", acceptanceURL);
203                         request.setAttribute("target", request.getParameter("target"));
204
205                         SAMLResponse samlResponse = new SAMLResponse(null, acceptanceURL, Arrays.asList(assertions), null);
206                         IdPProtocolSupport.addSignatures(samlResponse, relyingParty, provider, true);
207                         createPOSTForm(request, response, samlResponse.toBase64());
208
209                         // Make transaction log entry
210                         if (relyingParty.isLegacyProvider()) {
211                                 support.getTransactionLog().info(
212                                                 "Authentication assertion issued to legacy provider (SHIRE: " + request.getParameter("shire")
213                                                                 + ") on behalf of principal (" + username + ") for resource ("
214                                                                 + request.getParameter("target") + "). Name Identifier: (" + nameId.getName()
215                                                                 + "). Name Identifier Format: (" + nameId.getFormat() + ").");
216                         } else {
217                                 support.getTransactionLog().info(
218                                                 "Authentication assertion issued to provider ("
219                                                                 + relyingParty.getIdentityProvider().getProviderId() + ") on behalf of principal ("
220                                                                 + username + "). Name Identifier: (" + nameId.getName()
221                                                                 + "). Name Identifier Format: (" + nameId.getFormat() + ").");
222                         }
223                 }
224                 return null;
225         }
226
227         SAMLAssertion[] foo(HttpServletRequest request, RelyingParty relyingParty, EntityDescriptor provider,
228                         SAMLNameIdentifier nameId, String authenticationMethod, Date authTime) throws SAMLException, IOException {
229
230                 Document doc = org.opensaml.XML.parserPool.newDocument();
231
232                 // Determine audiences and issuer
233                 ArrayList audiences = new ArrayList();
234                 if (relyingParty.getProviderId() != null) {
235                         audiences.add(relyingParty.getProviderId());
236                 }
237                 if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
238                         audiences.add(relyingParty.getName());
239                 }
240
241                 String issuer = null;
242                 if (relyingParty.isLegacyProvider()) {
243                         // TODO figure this out
244                         /*
245                          * log.debug("Service Provider is running Shibboleth <= 1.1. Using old style issuer."); if
246                          * (relyingParty.getIdentityProvider().getResponseSigningCredential() == null ||
247                          * relyingParty.getIdentityProvider().getResponseSigningCredential().getX509Certificate() == null) { throw
248                          * new SAMLException( "Cannot serve legacy style assertions without an X509 certificate"); } issuer =
249                          * ShibBrowserProfile.getHostNameFromDN(relyingParty.getIdentityProvider()
250                          * .getResponseSigningCredential().getX509Certificate().getSubjectX500Principal()); if (issuer == null ||
251                          * issuer.equals("")) { throw new SAMLException( "Error parsing certificate DN while determining legacy
252                          * issuer name."); }
253                          */
254                 } else {
255                         issuer = relyingParty.getIdentityProvider().getProviderId();
256                 }
257
258                 // For compatibility with pre-1.2 shibboleth targets, include a pointer to the AA
259                 ArrayList bindings = new ArrayList();
260                 if (relyingParty.isLegacyProvider()) {
261
262                         SAMLAuthorityBinding binding = new SAMLAuthorityBinding(SAMLBinding.SOAP, relyingParty.getAAUrl()
263                                         .toString(), new QName(org.opensaml.XML.SAMLP_NS, "AttributeQuery"));
264                         bindings.add(binding);
265                 }
266
267                 // Create the authN assertion
268                 Vector conditions = new Vector(1);
269                 if (audiences != null && audiences.size() > 0) conditions.add(new SAMLAudienceRestrictionCondition(audiences));
270
271                 String[] confirmationMethods = {SAMLSubject.CONF_BEARER};
272                 SAMLSubject subject = new SAMLSubject(nameId, Arrays.asList(confirmationMethods), null, null);
273
274                 SAMLStatement[] statements = {new SAMLAuthenticationStatement(subject, authenticationMethod, authTime, request
275                                 .getRemoteAddr(), null, bindings)};
276
277                 SAMLAssertion[] assertions = {new SAMLAssertion(issuer, new Date(System.currentTimeMillis()), new Date(System
278                                 .currentTimeMillis() + 300000), conditions, null, Arrays.asList(statements))};
279
280                 if (log.isDebugEnabled()) {
281                         log.debug("Dumping generated SAML Assertions:"
282                                         + System.getProperty("line.separator")
283                                         + new String(new BASE64Decoder().decodeBuffer(new String(assertions[0].toBase64(), "ASCII")),
284                                                         "UTF8"));
285                 }
286
287                 return assertions;
288         }
289
290         /*
291          * @see edu.internet2.middleware.shibboleth.idp.IdPResponder.ProtocolHandler#getHandlerName()
292          */
293         public String getHandlerName() {
294
295                 return "Shibboleth v1.x SSO";
296         }
297
298         private static void createPOSTForm(HttpServletRequest req, HttpServletResponse res, byte[] buf) throws IOException,
299                         ServletException {
300
301                 // Hardcoded to ASCII to ensure Base64 encoding compatibility
302                 req.setAttribute("assertion", new String(buf, "ASCII"));
303
304                 if (log.isDebugEnabled()) {
305                         try {
306                                 log.debug("Dumping generated SAML Response:" + System.getProperty("line.separator")
307                                                 + new String(new BASE64Decoder().decodeBuffer(new String(buf, "ASCII")), "UTF8"));
308                         } catch (IOException e) {
309                                 log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
310                         }
311                 }
312
313                 RequestDispatcher rd = req.getRequestDispatcher("/IdP.jsp");
314                 rd.forward(req, res);
315         }
316
317         private static boolean useArtifactProfile(EntityDescriptor provider, String acceptanceURL) {
318
319                 // Default to POST if we have no metadata
320                 if (provider == null) { return false; }
321
322                 // Default to POST if we have incomplete metadata
323                 SPSSODescriptor sp = provider.getSPSSODescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
324                 if (sp == null) { return false; }
325
326                 // TODO: This will actually favor artifact, since a given location could support
327                 // both profiles. If that's not what we want, needs adjustment...
328                 Iterator endpoints = sp.getAssertionConsumerServiceManager().getEndpoints();
329                 while (endpoints.hasNext()) {
330                         Endpoint ep = (Endpoint) endpoints.next();
331                         if (acceptanceURL.equals(ep.getLocation())
332                                         && SAMLBrowserProfile.PROFILE_ARTIFACT_URI.equals(ep.getBinding())) { return true; }
333                 }
334
335                 // Default to POST if we have incomplete metadata
336                 return false;
337         }
338
339         private static boolean isValidAssertionConsumerURL(EntityDescriptor provider, String shireURL)
340                         throws InvalidClientDataException {
341
342                 SPSSODescriptor sp = provider.getSPSSODescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
343                 if (sp == null) {
344                         log.info("Inappropriate metadata for provider.");
345                         return false;
346                 }
347
348                 Iterator endpoints = sp.getAssertionConsumerServiceManager().getEndpoints();
349                 while (endpoints.hasNext()) {
350                         if (shireURL.equals(((Endpoint) endpoints.next()).getLocation())) { return true; }
351                 }
352                 log.info("Supplied consumer URL not found in metadata.");
353                 return false;
354         }
355 }