Implement SP authn by credential names via metadata, dump 1.1 legacy mode.
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / idp / provider / E_AuthSSOHandler.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.URLEncoder;
21 import java.security.Principal;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.Date;
25 import java.util.Iterator;
26 import java.util.List;
27
28 import javax.servlet.ServletException;
29 import javax.servlet.http.Cookie;
30 import javax.servlet.http.HttpServletRequest;
31 import javax.servlet.http.HttpServletResponse;
32
33 import org.apache.log4j.Logger;
34 import org.opensaml.SAMLAssertion;
35 import org.opensaml.SAMLAttribute;
36 import org.opensaml.SAMLAttributeStatement;
37 import org.opensaml.SAMLAuthenticationStatement;
38 import org.opensaml.SAMLException;
39 import org.opensaml.SAMLNameIdentifier;
40 import org.opensaml.SAMLRequest;
41 import org.opensaml.SAMLResponse;
42 import org.opensaml.SAMLStatement;
43 import org.opensaml.SAMLSubject;
44 import org.opensaml.artifact.Artifact;
45 import org.w3c.dom.Element;
46
47 import edu.internet2.middleware.shibboleth.aa.AAException;
48 import edu.internet2.middleware.shibboleth.common.LocalPrincipal;
49 import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException;
50 import edu.internet2.middleware.shibboleth.common.RelyingParty;
51 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
52 import edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler;
53 import edu.internet2.middleware.shibboleth.idp.IdPProtocolSupport;
54 import edu.internet2.middleware.shibboleth.idp.InvalidClientDataException;
55 import edu.internet2.middleware.shibboleth.metadata.Endpoint;
56 import edu.internet2.middleware.shibboleth.metadata.EntityDescriptor;
57 import edu.internet2.middleware.shibboleth.metadata.SPSSODescriptor;
58
59 /**
60  * <code>ProtocolHandler</code> implementation that responds to SSO flows as specified in "E-Authentication Interface
61  * Specifications for the SAML Artifact Profile ".
62  * 
63  * @author Walter Hoehn
64  */
65 public class E_AuthSSOHandler extends SSOHandler implements IdPProtocolHandler {
66
67         private static Logger log = Logger.getLogger(E_AuthSSOHandler.class.getName());
68         private final static String E_AUTH_NAMEID = "urn:oasis:names:tc:SAML:1.0:assertion#X509SubjectName";
69         private final static String E_AUTH_ATTR_NAMESPACE = "http://eauthentication.gsa.gov/federated/attribute";
70         private String eAuthPortal = "http://eauth.firstgov.gov/service/select";
71         private String eAuthError = "http://eauth.firstgov.gov/service/error";
72         private String csid;
73         private int defaultAssuranceLevel = 1;
74
75         /**
76          * Required DOM-based constructor.
77          */
78         public E_AuthSSOHandler(Element config) throws ShibbolethConfigurationException {
79
80                 super(config);
81                 csid = config.getAttribute("csid");
82                 if (csid == null || csid.equals("")) {
83                         log.error("(csid) attribute is required for the " + getHandlerName() + "protocol handler.");
84                         throw new ShibbolethConfigurationException("Unable to initialize protocol handler.");
85                 }
86
87                 String portal = config.getAttribute("eAuthPortal");
88                 if (portal != null && !portal.equals("")) {
89                         eAuthPortal = portal;
90                 }
91
92                 String error = config.getAttribute("eAuthError");
93                 if (error != null && !error.equals("")) {
94                         eAuthError = portal;
95                 }
96
97                 String rawAssurance = config.getAttribute("defaultAssuranceLevel");
98                 if (rawAssurance != null && !rawAssurance.equals("")) {
99                         try {
100                                 defaultAssuranceLevel = Integer.parseInt(rawAssurance);
101                                 if (defaultAssuranceLevel < 1 || defaultAssuranceLevel > 5) { throw new NumberFormatException(); }
102                         } catch (NumberFormatException e) {
103                                 log.error("E-Authentication (defaultAssuranceLevel) attribute must be an integer between 1 & 5.");
104                                 throw new ShibbolethConfigurationException("Unable to initialize protocol handler.");
105                         }
106                 }
107         }
108
109         /*
110          * @see edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler#getHandlerName()
111          */
112         public String getHandlerName() {
113
114                 return "E-Authentication SSO";
115         }
116
117         /*
118          * @see edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler#processRequest(javax.servlet.http.HttpServletRequest,
119          *      javax.servlet.http.HttpServletResponse, org.opensaml.SAMLRequest,
120          *      edu.internet2.middleware.shibboleth.idp.IdPProtocolSupport)
121          */
122         public SAMLResponse processRequest(HttpServletRequest request, HttpServletResponse response,
123                         SAMLRequest samlRequest, IdPProtocolSupport support) throws SAMLException, IOException, ServletException {
124
125                 // Sanity check
126                 if (samlRequest != null) {
127                         log.error("Protocol Handler received a SAML Request, but is unable to handle it.");
128                         throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
129                 }
130
131                 // If no aaid is specified, redirect to the eAuth portal
132                 if (request.getParameter("aaid") == null || request.getParameter("aaid").equals("")) {
133                         log.debug("Received an E-Authentication request with no (aaid) parameter.  "
134                                         + "Redirecting to the E-Authentication portal.");
135                         response.sendRedirect(eAuthPortal + "?csid=" + csid);
136                         return null;
137                 }
138
139                 // FUTURE at some point this needs to be integrated with SAML2 session reset
140                 // If session reset was requested, delete the session and re-direct back
141                 // Note, this only works with servler form-auth
142                 String reAuth = request.getParameter("sessionreset");
143                 if (reAuth != null && reAuth.equals("1")) {
144                         log.debug("E-Authebtication session reset requested.");
145                         Cookie session = new Cookie("JSESSIONID", null);
146                         session.setMaxAge(0);
147                         response.addCookie(session);
148
149                         response.sendRedirect(request.getRequestURI()
150                                         + (request.getQueryString() != null ? "?"
151                                                         + request.getQueryString().replaceAll("(^sessionreset=1&?|&?sessionreset=1)", "") : ""));
152                         return null;
153                 }
154                 // Sanity check
155                 try {
156                         validateEngineData(request);
157                 } catch (InvalidClientDataException e) {
158                         throw new SAMLException(SAMLException.RESPONDER, e.getMessage());
159                 }
160
161                 // Get the authN info
162                 String username = support.getIdPConfig().getAuthHeaderName().equalsIgnoreCase("REMOTE_USER") ? request
163                                 .getRemoteUser() : request.getHeader(support.getIdPConfig().getAuthHeaderName());
164                 if ((username == null) || (username.equals(""))) {
165                         log.error("Unable to authenticate remote user.");
166                         throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
167                 }
168                 LocalPrincipal principal = new LocalPrincipal(username);
169
170                 // Select the appropriate Relying Party configuration for the request
171                 String remoteProviderId = request.getParameter("aaid");
172                 log.debug("Remote provider has identified itself as: (" + remoteProviderId + ").");
173                 RelyingParty relyingParty = support.getServiceProviderMapper().getRelyingParty(remoteProviderId);
174
175                 if (relyingParty == null || relyingParty.isLegacyProvider()) {
176                         log.error("Unable to identify appropriate relying party configuration.");
177                         eAuthError(response, 30, remoteProviderId, csid);
178                         return null;
179                 }
180
181                 // Lookup the provider in the metadata
182                 EntityDescriptor entity = support.lookup(relyingParty.getProviderId());
183                 if (entity == null) {
184                         log.error("No metadata found for EAuth provider.");
185                         eAuthError(response, 30, remoteProviderId, csid);
186                         return null;
187                 }
188                 SPSSODescriptor role = entity.getSPSSODescriptor("urn:oasis:names:tc:SAML:1.1:protocol");
189                 if (role == null) {
190                         log.error("Inappropriate metadata for EAuth provider.");
191                         eAuthError(response, 30, remoteProviderId, csid);
192                         return null;
193                 }
194
195                 // The EAuth profile requires metadata, since the assertion consumer is not supplied as a request parameter
196                 // Pull the consumer URL from the metadata
197                 Iterator endpoints = role.getAssertionConsumerServiceManager().getEndpoints();
198                 if (endpoints == null || !endpoints.hasNext()) {
199                         log.error("Inappropriate metadata for provider: no roles specified.");
200                         eAuthError(response, 30, remoteProviderId, csid);
201                         return null;
202                 }
203                 String consumerURL = ((Endpoint) endpoints.next()).getLocation();
204                 log.debug("Assertion Consumer URL provider: " + consumerURL);
205
206                 // Create SAML Name Identifier & Subject
207                 SAMLNameIdentifier nameId;
208                 try {
209                         nameId = getNameIdentifier(support.getNameMapper(), principal, relyingParty, entity);
210                         if (!nameId.getFormat().equals(E_AUTH_NAMEID)) {
211                                 log.error("SAML Name Identifier format is inappropriate for use with E-Authentication provider.  Was ("
212                                                 + nameId.getFormat() + ").  Expected (" + E_AUTH_NAMEID + ").");
213                                 eAuthError(response, 60, remoteProviderId, csid);
214                                 return null;
215                         }
216                 } catch (NameIdentifierMappingException e) {
217                         log.error("Error converting principal to SAML Name Identifier: " + e);
218                         eAuthError(response, 60, remoteProviderId, csid);
219                         return null;
220                 }
221
222                 String[] confirmationMethods = {SAMLSubject.CONF_ARTIFACT};
223                 SAMLSubject authNSubject = new SAMLSubject(nameId, Arrays.asList(confirmationMethods), null, null);
224
225                 // Determine AuthN method
226                 String authenticationMethod = request.getHeader("SAMLAuthenticationMethod");
227                 if (authenticationMethod == null || authenticationMethod.equals("")) {
228                         authenticationMethod = relyingParty.getDefaultAuthMethod().toString();
229                         log.debug("User was authenticated via the default method for this relying party (" + authenticationMethod
230                                         + ").");
231                 } else {
232                         log.debug("User was authenticated via the method (" + authenticationMethod + ").");
233                 }
234
235                 String issuer = relyingParty.getIdentityProvider().getProviderId();
236
237                 log.info("Resolving attributes.");
238                 List attributes = null;
239                 try {
240                         attributes = Arrays.asList(support.getReleaseAttributes(principal, relyingParty, relyingParty
241                                         .getProviderId(), null));
242                 } catch (AAException e1) {
243                         log.error("Error resolving attributes: " + e1);
244                         eAuthError(response, 90, remoteProviderId, csid);
245                         return null;
246                 }
247                 log.info("Found " + attributes.size() + " attribute(s) for " + principal.getName());
248
249                 // Bail if we didn't get any attributes
250                 if (attributes == null || attributes.size() < 1) {
251                         log.error("Attribute resolver did not return any attributes. "
252                                         + " The E-Authentication profile's minimum attribute requirements were not met.");
253                         eAuthError(response, 60, remoteProviderId, csid);
254                         return null;
255
256                         // OK, we got attributes back, package them as required for eAuth and combine them with the authN data in an
257                         // assertion
258                 } else {
259                         try {
260                                 attributes = repackageForEauth(attributes);
261                         } catch (SAMLException e) {
262                                 eAuthError(response, 90, remoteProviderId, csid);
263                                 return null;
264                         }
265
266                         // Put all attributes into an assertion
267                         try {
268                                 SAMLStatement attrStatement = new SAMLAttributeStatement((SAMLSubject) authNSubject.clone(), attributes);
269                                 SAMLStatement[] statements = {
270                                                 new SAMLAuthenticationStatement(authNSubject, authenticationMethod, getAuthNTime(request),
271                                                                 request.getRemoteAddr(), null, null), attrStatement};
272                                 SAMLAssertion assertion = new SAMLAssertion(issuer, new Date(System.currentTimeMillis()), new Date(
273                                                 System.currentTimeMillis() + 300000), null, null, Arrays.asList(statements));
274                                 if (log.isDebugEnabled()) {
275                                         log.debug("Dumping generated SAML Assertion:" + System.getProperty("line.separator")
276                                                         + assertion.toString());
277                                 }
278
279                                 // Redirect to agency application
280                                 try {
281                                         respondWithArtifact(response, support, consumerURL, principal, assertion, nameId, role,
282                                                         relyingParty);
283                                         return null;
284                                 } catch (SAMLException e) {
285                                         eAuthError(response, 90, remoteProviderId, csid);
286                                         return null;
287                                 }
288
289                         } catch (CloneNotSupportedException e) {
290                                 log.error("An error was encountered while generating assertion: " + e);
291                                 eAuthError(response, 90, remoteProviderId, csid);
292                                 return null;
293                         }
294                 }
295         }
296
297         private void respondWithArtifact(HttpServletResponse response, IdPProtocolSupport support, String acceptanceURL,
298                         Principal principal, SAMLAssertion assertion, SAMLNameIdentifier nameId, SPSSODescriptor descriptor,
299                         RelyingParty relyingParty) throws SAMLException, IOException {
300
301                 // Create artifacts for each assertion
302                 ArrayList artifacts = new ArrayList();
303
304                 artifacts.add(support.getArtifactMapper().generateArtifact(assertion, relyingParty));
305
306                 String target = relyingParty.getDefaultTarget();
307                 if (target == null || target.equals("")) {
308                         log.error("No default target found.  Relying Party elements corresponding to "
309                                         + "E-Authentication providers must have a (defaultTarget) attribute specified.");
310                         throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
311                 }
312
313                 // Assemble the query string
314                 StringBuffer destination = new StringBuffer(acceptanceURL);
315                 destination.append("?TARGET=");
316                 destination.append(URLEncoder.encode(target, "UTF-8"));
317                 Iterator iterator = artifacts.iterator();
318                 StringBuffer artifactBuffer = new StringBuffer(); // Buffer for the transaction log
319
320                 // Construct the artifact query parameter
321                 while (iterator.hasNext()) {
322                         Artifact artifact = (Artifact) iterator.next();
323                         artifactBuffer.append("(" + artifact.encode() + ")");
324                         destination.append("&SAMLart=");
325                         destination.append(URLEncoder.encode(artifact.encode(), "UTF-8"));
326                 }
327
328                 log.debug("Redirecting to (" + destination.toString() + ").");
329                 response.sendRedirect(destination.toString()); // Redirect to the artifact receiver
330                 support.getTransactionLog().info(
331                                 "Assertion artifact(s) (" + artifactBuffer.toString() + ") issued to E-Authentication provider ("
332                                                 + relyingParty.getProviderId() + ") on behalf of principal (" + principal.getName()
333                                                 + "). Name Identifier: (" + nameId.getName() + "). Name Identifier Format: ("
334                                                 + nameId.getFormat() + ").");
335
336         }
337
338         private List repackageForEauth(List attributes) throws SAMLException {
339
340                 ArrayList writeable = new ArrayList(attributes);
341                 // Bail if we didn't get a commonName, because it is required by the profile
342                 SAMLAttribute commonName = getAttribute("commonName", writeable);
343                 if (commonName == null) {
344                         log.error("The attribute resolver did not return a (commonName) attribute, "
345                                         + " which is required for the E-Authentication profile.");
346                         throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
347                 } else if (!E_AUTH_ATTR_NAMESPACE.equals(commonName.getNamespace())) {
348                         log.warn("The (commonName) attribute seems to have an incorrect namespace set.  It should be ("
349                                         + E_AUTH_ATTR_NAMESPACE + "), but it is currently set to " + commonName.getNamespace() + ").");
350                 }
351                 writeable.add(new SAMLAttribute("csid", E_AUTH_ATTR_NAMESPACE, null, 0, Arrays.asList(new String[]{csid})));
352
353                 // Pull assurance level from the resolver, if it is available
354                 // If it isn't, use the handler default
355                 SAMLAttribute assuranceLevel = getAttribute("assuranceLevel", writeable);
356                 if (assuranceLevel == null) {
357                         writeable.add(new SAMLAttribute("assuranceLevel", "http://eauthentication.gsa.gov/federated/attribute",
358                                         null, 0, Arrays.asList(new String[]{Integer.toString(defaultAssuranceLevel)})));
359                 } else {
360                         log.debug("Using user-specifc assuranceLevel override.");
361                 }
362
363                 return writeable;
364         }
365
366         private SAMLAttribute getAttribute(String name, List attributes) {
367
368                 Iterator iterator = attributes.iterator();
369                 while (iterator.hasNext()) {
370                         SAMLAttribute attribute = (SAMLAttribute) iterator.next();
371                         if (attribute.getName().equals(name)) { return attribute; }
372                 }
373                 return null;
374         }
375
376         private void eAuthError(HttpServletResponse response, int code, String aaid, String csid) throws IOException {
377
378                 log.info("Redirecting to E-Authentication error page.");
379                 response.sendRedirect(eAuthError + "?aaid=" + aaid + "&csid=" + csid + "&errcode=" + code);
380         }
381 }