package scrum.server.admin; import ilarkesto.auth.OpenId; import ilarkesto.base.Str; import ilarkesto.base.Utl; import ilarkesto.base.time.DateAndTime; import ilarkesto.core.logging.Log; import ilarkesto.io.IO; import ilarkesto.ui.web.HtmlRenderer; import ilarkesto.webapp.Servlet; import java.io.IOException; import java.io.UnsupportedEncodingException; import javax.servlet.ServletConfig; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import scrum.client.ApplicationInfo; import scrum.client.ScrumGwtApplication; import scrum.server.ScrumConfig; import scrum.server.ScrumWebApplication; import scrum.server.WebSession; import scrum.server.common.AHttpServlet; public class LoginServlet extends AHttpServlet { private static final int LOGIN_TOKEN_COOKIE_MAXAGE = 259200; // 3days private static final long serialVersionUID = 1; private static Log log = Log.get(LoginServlet.class); private static ScrumWebApplication webApplication; private ApplicationInfo applicationInfo; private ScrumConfig config; private UserDao userDao; private SystemConfig systemConfig; @Override protected void onRequest(HttpServletRequest req, HttpServletResponse resp, WebSession session) throws IOException { String historyToken = req.getParameter("historyToken"); if (session.getUser() != null) { resp.sendRedirect(getStartPage(historyToken)); return; } String loginToken = Servlet.getCookieValue(req, ScrumGwtApplication.LOGIN_TOKEN_COOKIE); if (!Str.isBlank(loginToken)) { User user = userDao.getUserByLoginToken(loginToken); if (user != null) { user.setLastLoginDateAndTime(DateAndTime.now()); session.setUser(user); Servlet.setCookie(resp, ScrumGwtApplication.LOGIN_TOKEN_COOKIE, user.getLoginToken(), LOGIN_TOKEN_COOKIE_MAXAGE); resp.sendRedirect(getStartPage(historyToken)); return; } } if (OpenId.isOpenIdCallback(req)) { loginOpenId(resp, session, req); return; } if (req.getParameter("createAccount") != null) { createAccount(req.getParameter("username"), req.getParameter("email"), req.getParameter("password"), historyToken, resp, session); return; } if (req.getParameter("passwordRequest") != null) { passwordRequest(req.getParameter("email"), historyToken, resp, session); return; } String openId = req.getParameter("openid"); if (openId != null) { redirectOpenId(openId, req.getParameter("keepmeloggedin") != null, historyToken, resp, session, req); return; } String username = req.getParameter("username"); if (username != null) { login(username, req.getParameter("password"), req.getParameter("keepmeloggedin") != null, historyToken, resp, session); return; } renderLoginPage(resp, null, null, historyToken, null, req.getParameter("showPasswordRequest") != null, req.getParameter("showCreateAccount") != null); } private void passwordRequest(String login, String historyToken, HttpServletResponse resp, WebSession session) throws UnsupportedEncodingException, IOException { if (login == null || Str.isBlank(login)) { renderLoginPage(resp, login, null, historyToken, "E-Mail required.", true, false); return; } login = login.toLowerCase(); User user = null; if (login.contains("@")) { user = userDao.getUserByEmail(login); } if (user == null) { user = userDao.getUserByName(login); } if (user == null) { renderLoginPage(resp, login, login, historyToken, "User '" + login + "' does not exist.", true, false); return; } if (user.isAdmin()) { renderLoginPage(resp, login, login, historyToken, "Admins can not request new passwords.", true, false); return; } if (!user.isEmailVerified()) { renderLoginPage(resp, login, login, historyToken, "User '" + login + "' has no verified email. Please contact the admin: " + systemConfig.getAdminEmail(), true, false); return; } user.triggerNewPasswordRequest(); renderLoginPage(resp, login, login, historyToken, "New password has been sent to " + login, false, false); } private void createAccount(String username, String email, String password, String historyToken, HttpServletResponse resp, WebSession session) throws UnsupportedEncodingException, IOException { if (Str.isBlank(username)) username = null; if (Str.isBlank(email)) email = null; if (Str.isBlank(password)) password = null; if (username == null) { renderLoginPage(resp, username, email, historyToken, "Creating account failed. Username required.", false, true); return; } if (systemConfig.isUserEmailMandatory() && email == null) { renderLoginPage(resp, username, email, historyToken, "Creating account failed. E-Mail required.", false, true); return; } if (password == null) { renderLoginPage(resp, username, email, historyToken, "Creating account failed. Password required.", false, true); return; } if (Str.containsNonLetterOrDigit(username)) { renderLoginPage(resp, username, email, historyToken, "Creating account failed. Name '" + username + "' contains an illegal character. Only letters and digits allowed.", false, true); return; } if (email != null && !Str.isEmail(email)) { renderLoginPage(resp, username, email, historyToken, "Creating account failed. Illegal email address.", false, true); return; } if (userDao.getUserByName(username) != null) { renderLoginPage(resp, username, email, historyToken, "Creating account failed. Name '" + username + "' is already used.", false, true); log.warn("Registration failed. User name already exists:", username); return; } if (email != null && userDao.getUserByEmail(email) != null) { renderLoginPage(resp, username, email, historyToken, "Creating account failed. Email '" + email + "' is already used.", false, true); log.warn("Registration failed. User email already exists:", email); return; } User user = userDao.postUser(email, username, password); user.setLastLoginDateAndTime(DateAndTime.now()); user.triggerEmailVerification(); webApplication.triggerRegisterNotification(user); session.setUser(user); resp.sendRedirect(getStartPage(historyToken)); } private String getStartPage(String historyToken) { String url = webApplication.isDevelopmentMode() ? "index.html?gwt.codesvr=127.0.0.1:9997" : ""; if (historyToken != null) url += "#" + historyToken; url = webApplication.createUrl(url); return url; } private void loginOpenId(HttpServletResponse resp, WebSession session, HttpServletRequest request) throws UnsupportedEncodingException, IOException { HttpSession httpSession = request.getSession(); String historyToken = (String) httpSession.getAttribute("openidHistoryToken"); boolean keepmeloggedin = httpSession.getAttribute("openidKeepmeloggedin") != null; String openId; try { openId = OpenId.getIdentifierFromCallbackWithoutSuffix(request); } catch (RuntimeException ex) { log.error("OpenID authentication failed.", ex); renderLoginPage(resp, null, null, historyToken, "OpenID authentication failed: " + Str.format(Utl.getRootCause(ex)), false, false); return; } if (openId == null) { renderLoginPage(resp, null, null, historyToken, "OpenID authentication failed.", false, false); return; } log.info("User authenticated by OpenID:", openId); User user = userDao.getUserByOpenId(openId); if (user == null) { if (webApplication.getSystemConfig().isRegistrationDisabled()) { renderLoginPage(resp, null, null, historyToken, "There is no user with the OpenID " + openId + " and creating new users is disabled.", false, false); return; } if (userDao.getUserByOpenId(openId) != null) { renderLoginPage(resp, null, null, historyToken, "Creating account failed. OpenID '" + openId + "' is already used.", false, true); log.warn("Registration failed. OpenID already exists:", openId); return; } user = userDao.postUserWithOpenId(openId); webApplication.triggerRegisterNotification(user); } if (user.isDisabled()) { renderLoginPage(resp, null, null, historyToken, "User is disabled.", false, false); return; } user.setLastLoginDateAndTime(DateAndTime.now()); session.setUser(user); if (keepmeloggedin) Servlet.setCookie(resp, ScrumGwtApplication.LOGIN_TOKEN_COOKIE, user.getLoginToken(), LOGIN_TOKEN_COOKIE_MAXAGE); resp.sendRedirect(getStartPage(historyToken)); } private void redirectOpenId(String openId, boolean keepmeloggedin, String historyToken, HttpServletResponse resp, WebSession session, HttpServletRequest request) throws UnsupportedEncodingException, IOException { HttpSession httpSession = request.getSession(); if (Str.isBlank(openId)) openId = null; if (openId == null) { renderLoginPage(resp, null, null, historyToken, "Login failed. OpenID required.", false, true); return; } String url; try { url = OpenId.createAuthenticationRequestUrl(openId, webApplication.createUrl("login.html"), httpSession); } catch (RuntimeException ex) { log.error("OpenID authentication failed.", ex); renderLoginPage(resp, null, null, historyToken, "OpenID authentication failed: " + Str.format(Utl.getRootCause(ex)), false, false); return; } httpSession.setAttribute("openidHistoryToken", historyToken); httpSession.setAttribute("openidKeepmeloggedin", keepmeloggedin ? "true" : null); resp.sendRedirect(url); } private void login(String username, String password, boolean keepmeloggedin, String historyToken, HttpServletResponse resp, WebSession session) throws UnsupportedEncodingException, IOException { username = username.toLowerCase(); User user = null; if (username.contains("@")) { user = userDao.getUserByEmail(username); } if (user == null) { user = userDao.getUserByName(username); } if (user == null || user.matchesPassword(password) == false) { renderLoginPage(resp, username, null, historyToken, "Login failed.", false, false); return; } if (user.isDisabled()) { renderLoginPage(resp, username, null, historyToken, "User is disabled.", false, false); return; } user.setLastLoginDateAndTime(DateAndTime.now()); session.setUser(user); if (keepmeloggedin) Servlet.setCookie(resp, ScrumGwtApplication.LOGIN_TOKEN_COOKIE, user.getLoginToken(), LOGIN_TOKEN_COOKIE_MAXAGE); resp.sendRedirect(getStartPage(historyToken)); } private void renderLoginPage(HttpServletResponse resp, String username, String email, String historyToken, String message, boolean passwordRequest, boolean createAccount) throws UnsupportedEncodingException, IOException { String charset = IO.UTF_8; resp.setContentType("text/html"); HtmlRenderer html = new HtmlRenderer(resp.getOutputStream(), charset); html.startHTMLstandard(); html.startHEAD(applicationInfo.getName() + " Login", "EN"); html.META("X-UA-Compatible", "chrome=1"); html.LINKfavicon(); html.LINKcss("scrum.ScrumGwtApplication/screen.css"); html.endHEAD(); html.startBODY(); html.startDIV("loginPage"); html.startDIV("panel"); String logoUrl = webApplication.getSystemConfig().getLoginPageLogoUrl(); if (Str.isBlank(logoUrl)) logoUrl = "kunagi.png"; html.IMG(logoUrl, "Kunagi", null, null, null); html.DIV("separator", null); if (message != null) renderMessage(html, message); if (!createAccount && !passwordRequest) renderLogin(html, username, historyToken); if (passwordRequest) renderPasswordRequest(html, username, historyToken); if (createAccount) renderCreateAccount(html, username, email, historyToken); html.DIV("separator", null); html.startDIV("kunagiLink"); html.text("Kunagi " + webApplication.getReleaseLabel() + " | "); html.A("http://kunagi.org", "kunagi.org"); html.endDIV(); html.endDIV(); html.endDIV(); html.comment(applicationInfo.toString()); html.SCRIPTjavascript(null, "document.getElementById('username').focus();"); String analyticsId = config.getGoogleAnalyticsId(); if (analyticsId != null) html.googleAnalytics(analyticsId); html.endBODY(); html.endHTML(); html.flush(); } private void renderLogin(HtmlRenderer html, String username, String historyToken) { html.H2("Login with OpenID"); renderOpenIdLoginForm(html, historyToken); html.DIV("separator", null); html.H2("Login with Password"); renderRetroLoginForm(html, username, historyToken); html.BR(); html.A("login.html?showPasswordRequest=true", "Forgot your password?"); if (!systemConfig.isRegistrationDisabled()) { html.nbsp(); html.nbsp(); html.A("login.html?showCreateAccount=true", "Create new account"); } if (webApplication.isAdminPasswordDefault()) { html.DIV("separator", null); html.startDIV("configMessage"); html.html("<h2>Warning!</h2>The administrator user <code>admin</code> has the default password <code>" + scrum.client.admin.User.INITIAL_PASSWORD + "</code>. Please change it."); html.endDIV(); } if (systemConfig.isLoginPageMessageSet()) { html.DIV("separator", null); html.startDIV("configMessage"); html.html(systemConfig.getLoginPageMessage()); html.endDIV(); } } public void renderRetroLoginForm(HtmlRenderer html, String username, String historyToken) { html.startFORM(null, "loginForm", false); html.INPUThidden("historyToken", historyToken); html.startTABLE().setAlignCenter(); html.startTR(); html.startTD(); html.LABEL("username", "Username / E-Mail"); html.endTD(); html.startTD(); html.LABEL("password", "Password"); html.endTD(); html.endTR(); html.startTR(); html.startTD(); html.INPUTtext("username", "username", username, 80); html.endTD(); html.startTD(); html.INPUTpassword("password", "password", 80, ""); html.endTD(); html.endTR(); html.startTR(); html.startTD(); html.INPUTcheckbox("keepmeloggedin", "keepmeloggedin", false); html.LABEL("keepmeloggedin", "Keep me logged in"); html.endTD(); html.startTD().setAlignRight(); html.INPUTsubmit("login", "Login", null, 's'); html.endTD(); html.endTR(); html.endTABLE(); html.endFORM(); } public void renderOpenIdLoginForm(HtmlRenderer html, String historyToken) { renderOpenIdLink(OpenId.MYOPENID, "MyOpenID", historyToken, html); renderOpenIdLink(OpenId.GOOGLE, "Google", historyToken, html); renderOpenIdLink(OpenId.YAHOO, "Yahoo!", historyToken, html); renderOpenIdLink(OpenId.LAUNCHPAD, "Launchpad", historyToken, html); renderOpenIdLink(OpenId.AOL, "AOL", historyToken, html); renderOpenIdLink(OpenId.VERISIGN, "Verisign", historyToken, html); renderOpenIdLink(OpenId.WORDPRESS, "WordPress", historyToken, html); renderOpenIdLink(OpenId.FLICKR, "Flickr", historyToken, html); // renderOpenIdLink(OpenId.BLOGSPOT, "Blogger", historyToken, html); renderOpenIdLink(OpenId.MYVIDOOP, "Vidoop", historyToken, html); html.DIVclear(); html.BR(); html.startFORM(null, "openIdForm", false); html.INPUThidden("historyToken", historyToken); html.startTABLE().setAlignCenter(); html.startTR(); html.startTD(); html.LABEL("openid", "Custom OpenID"); html.endTD(); html.TD(""); html.endTR(); html.startTR(); html.startTD(null, 2); html.INPUTtext("openid", "openid", null, 80); html.endTD(); html.endTR(); html.startTR(); html.startTD(); html.INPUTcheckbox("keepmeloggedinOpenId", "keepmeloggedin", false); html.LABEL("keepmeloggedinOpenId", "Keep me logged in"); html.endTD(); html.startTD().setAlignRight(); html.INPUTsubmit("login", "Login", null, 's'); html.endTD(); html.endTR(); html.endTABLE(); html.endFORM(); } private void renderOpenIdLink(String openId, String label, String historyToken, HtmlRenderer html) { StringBuilder sb = new StringBuilder(); sb.append("login.html?openid=").append(Str.encodeUrlParameter(openId)); sb.append("&login=Login"); if (historyToken != null) sb.append("&historyToken=").append(Str.encodeUrlParameter(historyToken)); html.startA("openid", sb.toString()); html.startDIV("button"); html.text(label); html.endDIV(); html.endA(); } private void renderPasswordRequest(HtmlRenderer html, String username, String historyToken) { html.H2("Request new password"); html.startFORM(null, "passwordRequestForm", false); html.INPUThidden("historyToken", historyToken); html.startTABLE().setAlignCenter(); html.startTR(); html.startTD(); html.LABEL("email", "E-Mail"); html.endTD(); html.TD(" "); html.endTR(); html.startTR(); html.startTD(); html.INPUTtext("email", "email", username, 80); html.endTD(); html.startTD(); html.INPUTsubmit("passwordRequest", "Request password", null, 's'); html.endTD(); html.endTR(); html.endTABLE(); html.endFORM(); html.BR(); html.A("login.html", "Back to Login"); } private void renderCreateAccount(HtmlRenderer html, String username, String email, String historyToken) { html.H2("Create account"); html.startDIV("createAccount"); html.startFORM(null, "loginForm", false); html.INPUThidden("historyToken", historyToken); html.startTABLE().setAlignCenter(); html.startTR(); html.startTD(); html.LABEL("username", "Username"); html.endTD(); html.startTD(); html.INPUTtext("username", "username", username, 80); html.endTD(); html.endTR(); html.startTR(); html.startTD(); if (!webApplication.getSystemConfig().isUserEmailMandatory()) html.startDIV("optionalLabel"); html.LABEL("email", "E-Mail"); if (!webApplication.getSystemConfig().isUserEmailMandatory()) html.endDIV(); html.endTD(); html.startTD(); html.INPUTtext("email", "email", email, 80); html.endTD(); html.endTR(); html.startTR(); html.startTD(); html.LABEL("password", "Password"); html.endTD(); html.startTD(); html.INPUTpassword("password", "password", 80, ""); html.endTD(); html.endTR(); html.startTR(); html.TD(""); html.startTD(); html.INPUTsubmit("createAccount", "Create account", null, 's'); html.endTD(); html.endTR(); html.endTABLE(); html.endFORM(); html.endDIV(); html.BR(); html.A("login.html", "Back to Login"); if (systemConfig.isRegisterPageMessageSet()) { html.DIV("separator", null); html.startDIV("configMessage"); html.html(systemConfig.getRegisterPageMessage()); html.endDIV(); } } private void renderMessage(HtmlRenderer html, String message) { html.startDIV("message"); html.text(message); html.endDIV(); html.DIV("separator", null); } @Override protected void onInit(ServletConfig servletConfig) { super.onInit(servletConfig); webApplication = ScrumWebApplication.get(); userDao = webApplication.getUserDao(); applicationInfo = webApplication.getApplicationInfo(); config = webApplication.getConfig(); systemConfig = webApplication.getSystemConfig(); } }