Redirect to login for browser requests without a cookie present and also for requests...
[pithos] / src / gr / ebs / gss / server / Login.java
1 /*
2  * Copyright 2008, 2009 Electronic Business Systems Ltd.
3  *
4  * This file is part of GSS.
5  *
6  * GSS is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * GSS is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with GSS.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 package gr.ebs.gss.server;
20
21 import static gr.ebs.gss.server.configuration.GSSConfigurationFactory.getConfiguration;
22 import gr.ebs.gss.client.exceptions.DuplicateNameException;
23 import gr.ebs.gss.client.exceptions.ObjectNotFoundException;
24 import gr.ebs.gss.client.exceptions.RpcException;
25 import gr.ebs.gss.server.domain.Nonce;
26 import gr.ebs.gss.server.domain.User;
27 import gr.ebs.gss.server.ejb.ExternalAPI;
28
29 import java.io.IOException;
30 import java.io.PrintWriter;
31 import java.io.UnsupportedEncodingException;
32 import java.net.URI;
33 import java.net.URISyntaxException;
34 import java.net.URLEncoder;
35 import java.util.Date;
36 import java.util.Formatter;
37
38 import javax.naming.Context;
39 import javax.naming.InitialContext;
40 import javax.naming.NamingException;
41 import javax.rmi.PortableRemoteObject;
42 import javax.servlet.http.Cookie;
43 import javax.servlet.http.HttpServlet;
44 import javax.servlet.http.HttpServletRequest;
45 import javax.servlet.http.HttpServletResponse;
46
47 import org.apache.commons.codec.binary.Base64;
48 import org.apache.commons.logging.Log;
49 import org.apache.commons.logging.LogFactory;
50
51 /**
52  * The servlet that handles user logins.
53  *
54  * @author past
55  */
56 public class Login extends HttpServlet {
57         /**
58          * The request parameter name for the nonce.
59          */
60         private static final String NONCE_PARAM = "nonce";
61
62         /**
63          * The request parameter name for the URL to redirect
64          * to after authentication.
65          */
66         private static final String NEXT_URL_PARAM = "next";
67
68         /**
69          * The request parameter name for the GWT code server URL, used when
70          * debugging.
71          */
72         private static final String GWT_SERVER_PARAM = "gwt.codesvr";
73
74         /**
75          * The serial version UID of the class.
76          */
77         private static final long serialVersionUID = 1L;
78
79         /**
80          * The name of the authentication cookie.
81          */
82         public static final String AUTH_COOKIE = "_gss_a";
83
84         /**
85          * The separator character for the authentication cookie.
86          */
87         public static final char COOKIE_SEPARATOR = '|';
88
89         /**
90          * The name of the the webdav cookie.
91          */
92         public static final String WEBDAV_COOKIE = "_gss_wd";
93
94         /**
95          * The logger.
96          */
97         private static Log logger = LogFactory.getLog(Login.class);
98
99         /**
100          * A helper method that retrieves a reference to the ExternalAPI bean and
101          * stores it for future use.
102          *
103          * @return an ExternalAPI instance
104          * @throws RpcException in case an error occurs
105          */
106         private ExternalAPI getService() throws RpcException {
107                 try {
108                         final Context ctx = new InitialContext();
109                         final Object ref = ctx.lookup(getConfiguration().getString("externalApiPath"));
110                         return (ExternalAPI) PortableRemoteObject.narrow(ref, ExternalAPI.class);
111                 } catch (final NamingException e) {
112                         logger.error("Unable to retrieve the ExternalAPI EJB", e);
113                         throw new RpcException("An error occurred while contacting the naming service");
114                 }
115         }
116
117         /**
118          * Return the name of the service.
119          */
120         private String getServiceName() {
121                 return getConfiguration().getString("serviceName", "GSS");
122         }
123
124         @Override
125         public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
126                 // Fetch the next URL to display, if any.
127                 String nextUrl = request.getParameter(NEXT_URL_PARAM);
128                 // Fetch the supplied nonce, if any.
129                 String nonce = request.getParameter(NONCE_PARAM);
130                 String[] attrs = new String[] {"REMOTE_USER", "HTTP_SHIB_INETORGPERSON_DISPLAYNAME",
131                                         "HTTP_SHIB_INETORGPERSON_GIVENNAME", "HTTP_SHIB_PERSON_COMMONNAME",
132                                         "HTTP_SHIB_PERSON_SURNAME", "HTTP_SHIB_INETORGPERSON_MAIL",
133                                         "HTTP_SHIB_EP_UNSCOPEDAFFILIATION", "HTTP_PERSISTENT_ID"};
134                 StringBuilder buf = new StringBuilder("Shibboleth Attributes\n");
135                 for (String attr: attrs)
136                         buf.append(attr+": ").append(request.getAttribute(attr)).append('\n');
137                 logger.info(buf);
138                 if (logger.isDebugEnabled()) {
139                         buf = new StringBuilder("Shibboleth Attributes as bytes\n");
140                         for (String attr: attrs)
141                                 if (request.getAttribute(attr) != null)
142                                         buf.append(attr+": ").append(getHexString(request.getAttribute(attr).toString().getBytes("UTF-8"))).append('\n');
143                         logger.debug(buf);
144                 }
145                 User user = null;
146                 response.setContentType("text/html");
147                 Object usernameAttr = request.getAttribute("REMOTE_USER");
148                 Object nameAttr = request.getAttribute("HTTP_SHIB_INETORGPERSON_DISPLAYNAME");
149                 Object givennameAttr = request.getAttribute("HTTP_SHIB_INETORGPERSON_GIVENNAME"); // Multi-valued
150                 Object cnAttr = request.getAttribute("HTTP_SHIB_PERSON_COMMONNAME"); // Multi-valued
151                 Object snAttr = request.getAttribute("HTTP_SHIB_PERSON_SURNAME"); // Multi-valued
152                 Object mailAttr = request.getAttribute("HTTP_SHIB_INETORGPERSON_MAIL"); // Multi-valued
153                 Object userclassAttr = request.getAttribute("HTTP_SHIB_EP_UNSCOPEDAFFILIATION"); // Multi-valued
154                 Object persistentIdAttr = request.getAttribute("HTTP_PERSISTENT_ID");
155                 // Use a configured test username if found, as a shortcut for development deployments.
156                 String gwtServer = null;
157                 if (getConfiguration().getString("testUsername") != null) {
158                         usernameAttr = getConfiguration().getString("testUsername");
159                         // Fetch the GWT code server URL, if any.
160                         gwtServer = request.getParameter(GWT_SERVER_PARAM);
161                 }
162                 if (usernameAttr == null) {
163                         String authErrorUrl = "authenticationError.jsp";
164                         authErrorUrl += "?name=" + (nameAttr==null? "-": nameAttr.toString());
165                         authErrorUrl += "&givenname=" + (givennameAttr==null? "-": givennameAttr.toString());
166                         authErrorUrl += "&sn=" + (snAttr==null? "-": snAttr.toString());
167                         authErrorUrl += "&cn=" + (cnAttr==null? "-": cnAttr.toString());
168                         authErrorUrl += "&mail=" + (mailAttr==null? "-": mailAttr.toString());
169                         authErrorUrl += "&userclass=" + (userclassAttr==null? "-": userclassAttr.toString());
170                         response.sendRedirect(authErrorUrl);
171                         return;
172                 }
173                 String username = decodeAttribute(usernameAttr);
174                 String name;
175                 if (nameAttr != null && !nameAttr.toString().isEmpty())
176                         name = decodeAttribute(nameAttr);
177                 else if (cnAttr != null && !cnAttr.toString().isEmpty()) {
178                         name = decodeAttribute(cnAttr);
179                         if (name.indexOf(';') != -1)
180                                 name = name.substring(0, name.indexOf(';'));
181                 } else if (givennameAttr != null && snAttr != null && !givennameAttr.toString().isEmpty() && !snAttr.toString().isEmpty()) {
182                         String givenname = decodeAttribute(givennameAttr);
183                         if (givenname.indexOf(';') != -1)
184                                 givenname = givenname.substring(0, givenname.indexOf(';'));
185                         String sn = decodeAttribute(snAttr);
186                         if (sn.indexOf(';') != -1)
187                                 sn = sn.substring(0, sn.indexOf(';'));
188                         name = givenname + ' ' + sn;
189                 } else if (givennameAttr == null && snAttr != null && !snAttr.toString().isEmpty()) {
190                         name = decodeAttribute(snAttr);
191                         if (name.indexOf(';') != -1)
192                                 name = name.substring(0, name.indexOf(';'));
193                 } else
194                         name = username;
195                 String mail = mailAttr != null ? mailAttr.toString() : username;
196                 if (mail.indexOf(';') != -1)
197                         mail = mail.substring(0, mail.indexOf(';'));
198                 // XXX we are not using the user class currently
199                 String userclass = userclassAttr != null ? userclassAttr.toString() : "";
200                 if (userclass.indexOf(';') != -1)
201                         userclass = userclass.substring(0, userclass.indexOf(';'));
202                 String persistentId = persistentIdAttr != null ? persistentIdAttr.toString() : "";
203                 String idp = "";
204                 String idpid = "";
205                 if (!persistentId.isEmpty()) {
206                         int bang = persistentId.indexOf('!');
207                         if (bang > -1) {
208                                 idp = persistentId.substring(0, bang);
209                                 idpid = persistentId.substring(bang + 1);
210                         }
211                 }
212                 try {
213                         user = getService().findUser(username);
214                         if (user == null)
215                                 user = getService().createUser(username, name, mail, idp, idpid);
216                         if (!user.hasAcceptedPolicy()) {
217                                 String policyUrl = "policy.jsp";
218                                 if (request.getQueryString() != null)
219                                         policyUrl += "?user=" + username + "&" + request.getQueryString();
220                                 response.sendRedirect(policyUrl);
221                                 return;
222                         }
223                         user.setName(name);
224                         user.setEmail(mail);
225                         user.setIdentityProvider(idp);
226                         user.setIdentityProviderId(idpid);
227                         user.setLastLogin(new Date());
228                         if (user.getAuthToken() == null)
229                                 user = getService().updateUserToken(user.getId());
230                         // Set WebDAV password to token if it's never been set.
231                         if (user.getWebDAVPassword() == null || user.getWebDAVPassword().length() == 0) {
232                                 String tokenEncoded = new String(Base64.encodeBase64(user.getAuthToken()), "US-ASCII");
233                                 user.setWebDAVPassword(tokenEncoded);
234                         }
235                         getService().updateUser(user);
236                 } catch (RpcException e) {
237                         String error = "An error occurred while communicating with the service";
238                         logger.error(error, e);
239                         response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, error);
240                         return;
241                 } catch (DuplicateNameException e) {
242                         String error = "User with username " + username + " already exists";
243                         logger.error(error, e);
244                         response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, error);
245                         return;
246                 } catch (ObjectNotFoundException e) {
247                         String error = "No username was provided";
248                         logger.error(error, e);
249                         response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, error);
250                         return;
251                 }
252                 String tokenEncoded = new String(Base64.encodeBase64(user.getAuthToken()), "US-ASCII");
253                 String userEncoded = URLEncoder.encode(user.getUsername(), "US-ASCII");
254                 if (logger.isDebugEnabled())
255                         logger.debug("user: "+userEncoded+" token: "+tokenEncoded);
256                 if (nextUrl != null && !nextUrl.isEmpty()) {
257                         URI next;
258                         if (gwtServer != null)
259                                 nextUrl += '?' + GWT_SERVER_PARAM + '=' + gwtServer;
260                         try {
261                                 next = new URI(nextUrl);
262                         } catch (URISyntaxException e) {
263                                 response.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
264                                 return;
265                         }
266                         if ("x-gr-ebs-igss".equalsIgnoreCase(next.getScheme()))
267                                 nextUrl += "?u=" + userEncoded + "&t=" + tokenEncoded;
268                         else {
269                                 String domain = next.getHost();
270                                 String path = getServletContext().getContextPath() + '/';
271                                 Cookie cookie = new Cookie(AUTH_COOKIE, userEncoded + COOKIE_SEPARATOR +
272                                                         tokenEncoded);
273                                 cookie.setMaxAge(-1);
274                                 cookie.setDomain(domain);
275                                 cookie.setPath(path);
276                             response.addCookie(cookie);
277                             cookie = new Cookie(WEBDAV_COOKIE, user.getWebDAVPassword());
278                                 cookie.setMaxAge(-1);
279                                 cookie.setDomain(domain);
280                                 cookie.setPath(path);
281                                 response.addCookie(cookie);
282                         }
283                     response.sendRedirect(nextUrl);
284                 } else if (nonce != null) {
285                         nonce = URLEncoder.encode(nonce, "US-ASCII");
286                         Nonce n = null;
287                         try {
288                                 if (logger.isDebugEnabled())
289                                         logger.debug("user: "+user.getId()+" nonce: "+nonce);
290                                 n = getService().getNonce(nonce, user.getId());
291                         } catch (ObjectNotFoundException e) {
292                             PrintWriter out = response.getWriter();
293                             out.println("<HTML>");
294                             out.println("<HEAD><TITLE>" + getServiceName() + " Authentication</TITLE>" +
295                                         "<LINK TYPE='text/css' REL='stylesheet' HREF='gss.css'></HEAD>");
296                             out.println("<BODY><CENTER><P>");
297                             out.println("The supplied nonce could not be found!");
298                             out.println("</CENTER></BODY></HTML>");
299                             return;
300                         } catch (RpcException e) {
301                                 String error = "An error occurred while communicating with the service";
302                                 logger.error(error, e);
303                                 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, error);
304                                 return;
305                         }
306                         try {
307                                 getService().activateUserNonce(user.getId(), nonce, n.getNonceExpiryDate());
308                         } catch (ObjectNotFoundException e) {
309                                 String error = "Unable to find user";
310                                 logger.error(error, e);
311                                 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, error);
312                                 return;
313                         } catch (RpcException e) {
314                                 String error = "An error occurred while communicating with the service";
315                                 logger.error(error, e);
316                                 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, error);
317                                 return;
318                         }
319                         try {
320                                 getService().removeNonce(n.getId());
321                         } catch (ObjectNotFoundException e) {
322                                 logger.info("Nonce already removed!", e);
323                         } catch (RpcException e) {
324                                 logger.warn("Could not remove nonce from data store", e);
325                         }
326                     PrintWriter out = response.getWriter();
327                     out.println("<HTML>");
328                     out.println("<HEAD><TITLE>" + getServiceName() + " Authentication</TITLE>" +
329                                 "<LINK TYPE='text/css' REL='stylesheet' HREF='gss.css'></HEAD>");
330                     out.println("<BODY><CENTER><P>");
331                     out.println("You can now close this browser window and return to your application.");
332                     out.println("</CENTER></BODY></HTML>");
333                 } else {
334                     PrintWriter out = response.getWriter();
335                     out.println("<HTML>");
336                     out.println("<HEAD><TITLE>" + getServiceName() + " Authentication</TITLE>" +
337                                 "<LINK TYPE='text/css' REL='stylesheet' HREF='gss.css'></HEAD>");
338                     out.println("<BODY><CENTER><P>");
339                     out.println("Name: " + user.getName() + "<BR>");
340                     out.println("E-mail: " + user.getEmail() + "<BR><P>");
341                     out.println("Username: " + user.getUsername() + "<BR>");
342                     out.println("Athentication token: " + tokenEncoded + "<BR>");
343                     out.println("</CENTER></BODY></HTML>");
344                 }
345         }
346
347         /**
348          * Decode the request attribute provided by the container to a UTF-8
349          * string, since GSS assumes all data to be encoded in UTF-8. The
350          * servlet container's encoding can be specified in gss.properties.
351          */
352         private String decodeAttribute(Object attribute) throws UnsupportedEncodingException {
353                 return new String(attribute.toString().getBytes(getConfiguration().getString("requestAttributeEncoding")), "UTF-8");
354         }
355
356         /**
357          * A helper method that converts a byte buffer to a printable list of
358          * hexadecimal numbers.
359          */
360         private String getHexString(byte[] buffer) {
361                 StringBuilder sb = new StringBuilder();
362                 Formatter formatter = new Formatter(sb);
363                 for (int i=0; i<buffer.length; i++)
364                         formatter.format("0x%x, ", buffer[i]);
365                 return sb.toString();
366         }
367
368 }