Java tutorial
/* * Copyright 2013-2019 Erudika. https://erudika.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * For issues and patches go to: https://github.com/erudika */ package com.erudika.scoold.controllers; import com.erudika.para.annotations.Email; import com.erudika.para.client.ParaClient; import com.erudika.para.core.Sysprop; import com.erudika.para.core.User; import com.erudika.para.utils.Config; import com.erudika.para.utils.Utils; import static com.erudika.scoold.ScooldServer.CONTEXT_PATH; import static com.erudika.scoold.ScooldServer.CSRF_COOKIE; import static com.erudika.scoold.ScooldServer.HOMEPAGE; import com.erudika.scoold.utils.HttpUtils; import com.erudika.scoold.utils.ScooldUtils; import java.util.Collections; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import static com.erudika.scoold.ScooldServer.SIGNINLINK; import java.util.Locale; import java.util.TimeZone; import java.util.concurrent.TimeUnit; import javax.ws.rs.core.HttpHeaders; import org.apache.commons.lang3.time.DateFormatUtils; import org.springframework.http.CacheControl; import org.springframework.http.ResponseEntity; @Controller public class SigninController { private final ScooldUtils utils; private final ParaClient pc; @Inject public SigninController(ScooldUtils utils) { this.utils = utils; this.pc = utils.getParaClient(); } @GetMapping("/signin") public String get(@RequestParam(name = "returnto", required = false, defaultValue = HOMEPAGE) String returnto, HttpServletRequest req, HttpServletResponse res, Model model) { if (utils.isAuthenticated(req)) { return "redirect:" + (StringUtils.startsWithIgnoreCase(returnto, SIGNINLINK) ? HOMEPAGE : returnto); } if (!HOMEPAGE.equals(returnto) && !SIGNINLINK.equals(returnto)) { HttpUtils.setStateParam("returnto", Utils.urlEncode(returnto), req, res); } else { HttpUtils.removeStateParam("returnto", req, res); } model.addAttribute("path", "signin.vm"); model.addAttribute("title", utils.getLang(req).get("signin.title")); model.addAttribute("signinSelected", "navbtn-hover"); model.addAttribute("fbLoginEnabled", !Config.FB_APP_ID.isEmpty()); model.addAttribute("gpLoginEnabled", !Config.getConfigParam("google_client_id", "").isEmpty()); model.addAttribute("ghLoginEnabled", !Config.GITHUB_APP_ID.isEmpty()); model.addAttribute("inLoginEnabled", !Config.LINKEDIN_APP_ID.isEmpty()); model.addAttribute("twLoginEnabled", !Config.TWITTER_APP_ID.isEmpty()); model.addAttribute("msLoginEnabled", !Config.MICROSOFT_APP_ID.isEmpty()); model.addAttribute("oa2LoginEnabled", !Config.getConfigParam("oa2_app_id", "").isEmpty()); model.addAttribute("ldapLoginEnabled", !Config.getConfigParam("security.ldap.server_url", "").isEmpty()); model.addAttribute("passwordLoginEnabled", Config.getConfigBoolean("password_auth_enabled", true)); model.addAttribute("oa2LoginProvider", Config.getConfigParam("security.oauth.provider", "Continue with OpenID Connect")); return "base"; } @GetMapping(path = "/signin", params = { "access_token", "provider" }) public String signinGet(@RequestParam("access_token") String accessToken, @RequestParam("provider") String provider, HttpServletRequest req, HttpServletResponse res) { return getAuth(provider, accessToken, req, res); } @PostMapping(path = "/signin", params = { "access_token", "provider" }) public String signinPost(@RequestParam("access_token") String accessToken, @RequestParam("provider") String provider, HttpServletRequest req, HttpServletResponse res) { return getAuth(provider, accessToken, req, res); } @GetMapping("/signin/success") public String signinSuccess(@RequestParam String jwt, HttpServletRequest req, HttpServletResponse res, Model model) { if (!StringUtils.isBlank(jwt) && !"?".equals(jwt)) { setAuthCookie(jwt, req, res); } else { return "redirect:" + SIGNINLINK + "?code=3&error=true"; } return "redirect:" + getBackToUrl(req); } @GetMapping(path = "/signin/register") public String register(@RequestParam(name = "verify", required = false, defaultValue = "false") Boolean verify, @RequestParam(name = "id", required = false) String id, @RequestParam(name = "token", required = false) String token, HttpServletRequest req, Model model) { if (utils.isAuthenticated(req)) { return "redirect:" + HOMEPAGE; } model.addAttribute("path", "signin.vm"); model.addAttribute("title", utils.getLang(req).get("signup.title")); model.addAttribute("signinSelected", "navbtn-hover"); model.addAttribute("emailPattern", Email.EMAIL_PATTERN); model.addAttribute("register", true); model.addAttribute("verify", verify); if (id != null && token != null) { boolean verified = activateWithEmailToken((User) pc.read(id), token); if (verified) { model.addAttribute("verified", verified); } else { return "redirect:" + SIGNINLINK; } } return "base"; } @PostMapping("/signin/register") public String signup(@RequestParam String name, @RequestParam String email, @RequestParam String passw, HttpServletRequest req, Model model) { boolean approvedDomain = utils.isEmailDomainApproved(email); if (!utils.isAuthenticated(req) && approvedDomain) { if (!isEmailRegistered(email)) { pc.signIn("password", email + ":" + name + ":" + passw, false); verifyEmailIfNecessary("password", name, email, req); } else { model.addAttribute("path", "signin.vm"); model.addAttribute("title", utils.getLang(req).get("signup.title")); model.addAttribute("signinSelected", "navbtn-hover"); model.addAttribute("register", true); model.addAttribute("name", name); model.addAttribute("bademail", email); model.addAttribute("error", Collections.singletonMap("email", utils.getLang(req).get("msgcode.1"))); return "base"; } } return "redirect:" + SIGNINLINK + (approvedDomain ? "/register?verify=true" : "?code=3&error=true"); } @GetMapping(path = "/signin/iforgot") public String iforgot(@RequestParam(name = "verify", required = false, defaultValue = "false") Boolean verify, @RequestParam(name = "email", required = false) String email, @RequestParam(name = "token", required = false) String token, HttpServletRequest req, Model model) { if (utils.isAuthenticated(req)) { return "redirect:" + HOMEPAGE; } model.addAttribute("path", "signin.vm"); model.addAttribute("title", utils.getLang(req).get("iforgot.title")); model.addAttribute("signinSelected", "navbtn-hover"); model.addAttribute("iforgot", true); model.addAttribute("verify", verify); if (email != null && token != null) { model.addAttribute("email", email); model.addAttribute("token", token); } return "base"; } @PostMapping("/signin/iforgot") public String changePass(@RequestParam String email, @RequestParam(required = false) String newpassword, @RequestParam(required = false) String token, HttpServletRequest req, Model model) { boolean approvedDomain = utils.isEmailDomainApproved(email); if (!utils.isAuthenticated(req) && approvedDomain) { if (StringUtils.isBlank(token)) { generatePasswordResetToken(email, req); return "redirect:" + SIGNINLINK + "/iforgot?verify=true"; } else { boolean error = !resetPassword(email, newpassword, token); model.addAttribute("path", "signin.vm"); model.addAttribute("title", utils.getLang(req).get("iforgot.title")); model.addAttribute("signinSelected", "navbtn-hover"); model.addAttribute("iforgot", true); model.addAttribute("email", email); model.addAttribute("token", ""); model.addAttribute("verified", !error); if (error) { model.addAttribute("error", Collections.singletonMap("email", utils.getLang(req).get("msgcode.7"))); } return "base"; } } return "redirect:" + SIGNINLINK + "/iforgot"; } @PostMapping("/signout") public String post(HttpServletRequest req, HttpServletResponse res) { if (utils.isAuthenticated(req)) { utils.clearSession(req, res); return "redirect:" + SIGNINLINK + "?code=5&success=true"; } return "redirect:" + HOMEPAGE; } @ResponseBody @GetMapping("/scripts/globals.js") public ResponseEntity<String> globals(HttpServletRequest req, HttpServletResponse res) { res.setContentType("text/javascript"); StringBuilder sb = new StringBuilder(); sb.append("APPID = \"").append(Config.getConfigParam("access_key", "app:scoold").substring(4)) .append("\"; "); sb.append("ENDPOINT = \"").append(pc.getEndpoint()).append("\"; "); sb.append("CONTEXT_PATH = \"").append(CONTEXT_PATH).append("\"; "); sb.append("CSRF_COOKIE = \"").append(CSRF_COOKIE).append("\"; "); sb.append("FB_APP_ID = \"").append(Config.FB_APP_ID).append("\"; "); sb.append("GOOGLE_CLIENT_ID = \"").append(Config.getConfigParam("google_client_id", "")).append("\"; "); sb.append("GOOGLE_ANALYTICS_ID = \"").append(Config.getConfigParam("google_analytics_id", "")) .append("\"; "); sb.append("GITHUB_APP_ID = \"").append(Config.GITHUB_APP_ID).append("\"; "); sb.append("LINKEDIN_APP_ID = \"").append(Config.LINKEDIN_APP_ID).append("\"; "); sb.append("TWITTER_APP_ID = \"").append(Config.TWITTER_APP_ID).append("\"; "); sb.append("MICROSOFT_APP_ID = \"").append(Config.MICROSOFT_APP_ID).append("\"; "); sb.append("OAUTH2_ENDPOINT = \"").append(Config.getConfigParam("security.oauth.authz_url", "")) .append("\"; "); sb.append("OAUTH2_APP_ID = \"").append(Config.getConfigParam("oa2_app_id", "")).append("\"; "); sb.append("OAUTH2_SCOPE = \"").append(Config.getConfigParam("security.oauth.scope", "")).append("\"; "); Locale currentLocale = utils.getCurrentLocale(utils.getLanguageCode(req), req); sb.append("RTL_ENABLED = ").append(utils.isLanguageRTL(currentLocale.getLanguage())).append("; "); String result = sb.toString(); return ResponseEntity.ok().cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS)).eTag(Utils.md5(result)) .body(result); } private String getAuth(String provider, String accessToken, HttpServletRequest req, HttpServletResponse res) { if (!utils.isAuthenticated(req)) { String email = getEmailFromAccessToken(accessToken); if ("password".equals(provider) && !isEmailRegistered(email)) { return "redirect:" + SIGNINLINK + "?code=3&error=true"; } User u = pc.signIn(provider, accessToken, false); if (u != null && utils.isEmailDomainApproved(u.getEmail())) { // the user password in this case is a Bearer token (JWT) setAuthCookie(u.getPassword(), req, res); } else { return "redirect:" + SIGNINLINK + "?code=3&error=true"; } } return "redirect:" + getBackToUrl(req); } private String getBackToUrl(HttpServletRequest req) { String backtoFromCookie = Utils.urlDecode(HttpUtils.getStateParam("returnto", req)); return (StringUtils.isBlank(backtoFromCookie) ? HOMEPAGE : backtoFromCookie); } private boolean activateWithEmailToken(User u, String token) { if (u != null && token != null) { Sysprop s = pc.read(u.getIdentifier()); if (s != null && token.equals(s.getProperty(Config._EMAIL_TOKEN))) { s.addProperty(Config._EMAIL_TOKEN, ""); pc.update(s); u.setActive(true); pc.update(u); return true; } } return false; } private String getEmailFromAccessToken(String accessToken) { String[] tokenParts = StringUtils.split(accessToken, ":"); return (tokenParts != null && tokenParts.length > 0) ? tokenParts[0] : ""; } private boolean isEmailRegistered(String email) { Sysprop ident = pc.read(email); return ident != null && ident.hasProperty(Config._PASSWORD); } private void verifyEmailIfNecessary(String provider, String name, String email, HttpServletRequest req) { if ("password".equals(provider)) { Sysprop ident = pc.read(email); if (ident != null && !ident.hasProperty(Config._EMAIL_TOKEN)) { User u = new User(ident.getCreatorid()); u.setActive(false); u.setName(name); u.setEmail(email); u.setIdentifier(email); utils.sendWelcomeEmail(u, true, req); } } } private String generatePasswordResetToken(String email, HttpServletRequest req) { if (StringUtils.isBlank(email)) { return ""; } Sysprop s = pc.read(email); if (s != null) { String token = Utils.generateSecurityToken(42, true); s.addProperty(Config._RESET_TOKEN, token); if (pc.update(s) != null) { utils.sendPasswordResetEmail(email, token, req); } return token; } return ""; } private boolean resetPassword(String email, String newpass, String token) { if (StringUtils.isBlank(newpass) || StringUtils.isBlank(token) || newpass.length() < Config.MIN_PASS_LENGTH) { return false; } Sysprop s = pc.read(email); if (isValidResetToken(s, Config._RESET_TOKEN, token)) { s.addProperty(Config._RESET_TOKEN, ""); // avoid removeProperty method because it won't be seen by server String hashed = Utils.bcrypt(newpass); s.addProperty(Config._PASSWORD, hashed); //setPassword(hashed); pc.update(s); return true; } return false; } private boolean isValidResetToken(Sysprop s, String key, String token) { if (StringUtils.isBlank(token)) { return false; } if (s != null && s.hasProperty(key)) { String storedToken = (String) s.getProperty(key); // tokens expire afer a reasonably short period ~ 30 mins long timeout = (long) Config.PASSRESET_TIMEOUT_SEC * 1000L; if (StringUtils.equals(storedToken, token) && (s.getUpdated() + timeout) > Utils.timestamp()) { return true; } } return false; } private void setAuthCookie(String jwt, HttpServletRequest req, HttpServletResponse res) { int maxAge = Config.SESSION_TIMEOUT_SEC; String expires = DateFormatUtils.format(System.currentTimeMillis() + (maxAge * 1000), "EEE, dd-MMM-yyyy HH:mm:ss z", TimeZone.getTimeZone("GMT")); StringBuilder sb = new StringBuilder(); sb.append(Config.AUTH_COOKIE).append("=").append(jwt).append(";"); sb.append("Path=/;"); sb.append("Expires=").append(expires).append(";"); sb.append("Max-Age=").append(maxAge).append(";"); sb.append("HttpOnly;"); sb.append("SameSite=Strict"); res.addHeader(HttpHeaders.SET_COOKIE, sb.toString()); } }