2 * Copyright 2008, 2009 Electronic Business Systems Ltd.
4 * This file is part of GSS.
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.
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.
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/>.
19 package gr.ebs.gss.server;
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;
29 import java.io.IOException;
30 import java.io.PrintWriter;
31 import java.io.UnsupportedEncodingException;
33 import java.net.URISyntaxException;
34 import java.net.URLEncoder;
35 import java.util.Formatter;
37 import javax.naming.Context;
38 import javax.naming.InitialContext;
39 import javax.naming.NamingException;
40 import javax.rmi.PortableRemoteObject;
41 import javax.servlet.http.Cookie;
42 import javax.servlet.http.HttpServlet;
43 import javax.servlet.http.HttpServletRequest;
44 import javax.servlet.http.HttpServletResponse;
46 import org.apache.commons.codec.binary.Base64;
47 import org.apache.commons.logging.Log;
48 import org.apache.commons.logging.LogFactory;
51 * The servlet that handles user logins.
55 public class Login extends HttpServlet {
57 * The request parameter name for the nonce.
59 private static final String NONCE_PARAM = "nonce";
62 * The request parameter name for the URL to redirect
63 * to after authentication.
65 private static final String NEXT_URL_PARAM = "next";
68 * The serial version UID of the class.
70 private static final long serialVersionUID = 1L;
73 * The name of the authentication cookie.
75 private static final String AUTH_COOKIE = "_gss_a";
78 * The separator character for the authentication cookie.
80 private static final char COOKIE_SEPARATOR = '|';
83 * The name of the the webdav cookie.
85 public static final String WEBDAV_COOKIE = "_gss_wd";
90 private static Log logger = LogFactory.getLog(Login.class);
93 * A helper method that retrieves a reference to the ExternalAPI bean and
94 * stores it for future use.
96 * @return an ExternalAPI instance
97 * @throws RpcException in case an error occurs
99 private ExternalAPI getService() throws RpcException {
101 final Context ctx = new InitialContext();
102 final Object ref = ctx.lookup(getConfiguration().getString("externalApiPath"));
103 return (ExternalAPI) PortableRemoteObject.narrow(ref, ExternalAPI.class);
104 } catch (final NamingException e) {
105 logger.error("Unable to retrieve the ExternalAPI EJB", e);
106 throw new RpcException("An error occurred while contacting the naming service");
111 * Return the name of the service.
113 private String getServiceName() {
114 return getConfiguration().getString("serviceName", "GSS");
118 public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
119 // Fetch the next URL to display, if any.
120 String nextUrl = request.getParameter(NEXT_URL_PARAM);
121 // Fetch the supplied nonce, if any.
122 String nonce = request.getParameter(NONCE_PARAM);
123 String[] attrs = new String[] {"REMOTE_USER", "HTTP_SHIB_INETORGPERSON_DISPLAYNAME",
124 "HTTP_SHIB_INETORGPERSON_GIVENNAME", "HTTP_SHIB_PERSON_COMMONNAME",
125 "HTTP_SHIB_PERSON_SURNAME", "HTTP_SHIB_INETORGPERSON_MAIL",
126 "HTTP_SHIB_EP_UNSCOPEDAFFILIATION"};
127 StringBuilder buf = new StringBuilder("Shibboleth Attributes\n");
128 for (String attr: attrs)
129 buf.append(attr+": ").append(request.getAttribute(attr)).append('\n');
131 if (logger.isDebugEnabled()) {
132 buf = new StringBuilder("Shibboleth Attributes as bytes\n");
133 for (String attr: attrs)
134 if (request.getAttribute(attr) != null)
135 buf.append(attr+": ").append(getHexString(request.getAttribute(attr).toString().getBytes("UTF-8"))).append('\n');
139 response.setContentType("text/html");
140 Object usernameAttr = request.getAttribute("REMOTE_USER");
141 Object nameAttr = request.getAttribute("HTTP_SHIB_INETORGPERSON_DISPLAYNAME");
142 Object givennameAttr = request.getAttribute("HTTP_SHIB_INETORGPERSON_GIVENNAME"); // Multi-valued
143 Object cnAttr = request.getAttribute("HTTP_SHIB_PERSON_COMMONNAME"); // Multi-valued
144 Object snAttr = request.getAttribute("HTTP_SHIB_PERSON_SURNAME"); // Multi-valued
145 Object mailAttr = request.getAttribute("HTTP_SHIB_INETORGPERSON_MAIL"); // Multi-valued
146 Object userclassAttr = request.getAttribute("HTTP_SHIB_EP_UNSCOPEDAFFILIATION"); // Multi-valued
147 // Use a configured test username if found, as a shortcut for development deployments.
148 if (getConfiguration().getString("testUsername") != null)
149 usernameAttr = getConfiguration().getString("testUsername");
150 if (usernameAttr == null) {
151 String authErrorUrl = "authenticationError.jsp";
152 authErrorUrl += "?name=" + (nameAttr==null? "-": nameAttr.toString());
153 authErrorUrl += "&givenname=" + (givennameAttr==null? "-": givennameAttr.toString());
154 authErrorUrl += "&sn=" + (snAttr==null? "-": snAttr.toString());
155 authErrorUrl += "&cn=" + (cnAttr==null? "-": cnAttr.toString());
156 authErrorUrl += "&mail=" + (mailAttr==null? "-": mailAttr.toString());
157 authErrorUrl += "&userclass=" + (userclassAttr==null? "-": userclassAttr.toString());
158 response.sendRedirect(authErrorUrl);
161 String username = decodeAttribute(usernameAttr);
163 if (nameAttr != null && !nameAttr.toString().isEmpty())
164 name = decodeAttribute(nameAttr);
165 else if (cnAttr != null && !cnAttr.toString().isEmpty()) {
166 name = decodeAttribute(cnAttr);
167 if (name.indexOf(';') != -1)
168 name = name.substring(0, name.indexOf(';'));
169 } else if (givennameAttr != null && snAttr != null && !givennameAttr.toString().isEmpty() && !snAttr.toString().isEmpty()) {
170 String givenname = decodeAttribute(givennameAttr);
171 if (givenname.indexOf(';') != -1)
172 givenname = givenname.substring(0, givenname.indexOf(';'));
173 String sn = decodeAttribute(snAttr);
174 if (sn.indexOf(';') != -1)
175 sn = sn.substring(0, sn.indexOf(';'));
176 name = givenname + ' ' + sn;
177 } else if (givennameAttr == null && snAttr != null && !snAttr.toString().isEmpty()) {
178 name = decodeAttribute(snAttr);
179 if (name.indexOf(';') != -1)
180 name = name.substring(0, name.indexOf(';'));
183 String mail = mailAttr != null ? mailAttr.toString() : username;
184 if (mail.indexOf(';') != -1)
185 mail = mail.substring(0, mail.indexOf(';'));
186 // XXX we are not using the user class currently
187 String userclass = userclassAttr != null ? userclassAttr.toString() : "";
188 if (userclass.indexOf(';') != -1)
189 userclass = userclass.substring(0, userclass.indexOf(';'));
191 user = getService().findUser(username);
193 user = getService().createUser(username, name, mail);
194 if (!user.hasAcceptedPolicy()) {
195 String policyUrl = "policy.jsp";
196 if (request.getQueryString() != null)
197 policyUrl += "?user=" + username + "&" + request.getQueryString();
198 response.sendRedirect(policyUrl);
201 // Update the user name and e-mail if modified.
202 boolean update = false;
203 if (!user.getName().equals(name)) {
207 if (!user.getEmail().equals(mail)) {
211 if (user.getAuthToken() == null)
212 user = getService().updateUserToken(user.getId());
213 // Set WebDAV password to token if it's never been set.
214 if (user.getWebDAVPassword()==null || user.getWebDAVPassword().length()==0) {
215 String tokenEncoded = new String(Base64.encodeBase64(user.getAuthToken()), "US-ASCII");
216 user.setWebDAVPassword(tokenEncoded);
220 getService().updateUser(user);
221 } catch (RpcException e) {
222 String error = "An error occurred while communicating with the service";
223 logger.error(error, e);
224 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, error);
226 } catch (DuplicateNameException e) {
227 String error = "User with username " + username + " already exists";
228 logger.error(error, e);
229 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, error);
231 } catch (ObjectNotFoundException e) {
232 String error = "No username was provided";
233 logger.error(error, e);
234 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, error);
237 String tokenEncoded = new String(Base64.encodeBase64(user.getAuthToken()), "US-ASCII");
238 String userEncoded = URLEncoder.encode(user.getUsername(), "US-ASCII");
239 if (logger.isDebugEnabled())
240 logger.debug("user: "+userEncoded+" token: "+tokenEncoded);
241 if (nextUrl != null && !nextUrl.isEmpty()) {
244 next = new URI(nextUrl);
245 } catch (URISyntaxException e) {
246 response.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
249 String domain = next.getHost();
250 String path = next.getPath();
251 Cookie cookie = new Cookie(AUTH_COOKIE, userEncoded + COOKIE_SEPARATOR +
253 cookie.setMaxAge(-1);
254 cookie.setDomain(domain);
255 cookie.setPath(path);
256 response.addCookie(cookie);
257 cookie = new Cookie(WEBDAV_COOKIE, user.getWebDAVPassword());
258 cookie.setMaxAge(-1);
259 cookie.setDomain(domain);
260 cookie.setPath(path);
261 response.addCookie(cookie);
262 response.sendRedirect(nextUrl);
263 } else if (nonce != null) {
264 nonce = URLEncoder.encode(nonce, "US-ASCII");
267 if (logger.isDebugEnabled())
268 logger.debug("user: "+user.getId()+" nonce: "+nonce);
269 n = getService().getNonce(nonce, user.getId());
270 } catch (ObjectNotFoundException e) {
271 PrintWriter out = response.getWriter();
272 out.println("<HTML>");
273 out.println("<HEAD><TITLE>" + getServiceName() + " Authentication</TITLE>" +
274 "<LINK TYPE='text/css' REL='stylesheet' HREF='gss.css'></HEAD>");
275 out.println("<BODY><CENTER><P>");
276 out.println("The supplied nonce could not be found!");
277 out.println("</CENTER></BODY></HTML>");
279 } catch (RpcException e) {
280 String error = "An error occurred while communicating with the service";
281 logger.error(error, e);
282 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, error);
286 getService().activateUserNonce(user.getId(), nonce, n.getNonceExpiryDate());
287 } catch (ObjectNotFoundException e) {
288 String error = "Unable to find user";
289 logger.error(error, e);
290 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, error);
292 } catch (RpcException e) {
293 String error = "An error occurred while communicating with the service";
294 logger.error(error, e);
295 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, error);
299 getService().removeNonce(n.getId());
300 } catch (ObjectNotFoundException e) {
301 logger.info("Nonce already removed!", e);
302 } catch (RpcException e) {
303 logger.warn("Could not remove nonce from data store", e);
305 PrintWriter out = response.getWriter();
306 out.println("<HTML>");
307 out.println("<HEAD><TITLE>" + getServiceName() + " Authentication</TITLE>" +
308 "<LINK TYPE='text/css' REL='stylesheet' HREF='gss.css'></HEAD>");
309 out.println("<BODY><CENTER><P>");
310 out.println("You can now close this browser window and return to your application.");
311 out.println("</CENTER></BODY></HTML>");
313 PrintWriter out = response.getWriter();
314 out.println("<HTML>");
315 out.println("<HEAD><TITLE>" + getServiceName() + " Authentication</TITLE>" +
316 "<LINK TYPE='text/css' REL='stylesheet' HREF='gss.css'></HEAD>");
317 out.println("<BODY><CENTER><P>");
318 out.println("Name: " + user.getName() + "<BR>");
319 out.println("E-mail: " + user.getEmail() + "<BR><P>");
320 out.println("Username: " + user.getUsername() + "<BR>");
321 out.println("Athentication token: " + tokenEncoded + "<BR>");
322 out.println("</CENTER></BODY></HTML>");
327 * Decode the request attribute provided by the container to a UTF-8
328 * string, since GSS assumes all data to be encoded in UTF-8. The
329 * servlet container's encoding can be specified in gss.properties.
331 private String decodeAttribute(Object attribute) throws UnsupportedEncodingException {
332 return new String(attribute.toString().getBytes(getConfiguration().getString("requestAttributeEncoding")), "UTF-8");
336 * A helper method that converts a byte buffer to a printable list of
337 * hexadecimal numbers.
339 private String getHexString(byte[] buffer) {
340 StringBuilder sb = new StringBuilder();
341 Formatter formatter = new Formatter(sb);
342 for (int i=0; i<buffer.length; i++)
343 formatter.format("0x%x, ", buffer[i]);
344 return sb.toString();