package org.fenixedu.bennu.portal.servlet; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; import javax.servlet.ServletConfig; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.fenixedu.bennu.core.util.CoreConfiguration; import org.fenixedu.bennu.portal.BennuPortalConfiguration; import org.fenixedu.bennu.portal.domain.PortalConfiguration; import org.fenixedu.bennu.portal.login.LoginProvider; import org.fenixedu.commons.i18n.I18N; import com.google.common.base.Strings; import com.mitchellbosecke.pebble.PebbleEngine; import com.mitchellbosecke.pebble.PebbleEngine.Builder; import com.mitchellbosecke.pebble.error.LoaderException; import com.mitchellbosecke.pebble.error.PebbleException; import com.mitchellbosecke.pebble.loader.ClasspathLoader; import com.mitchellbosecke.pebble.template.PebbleTemplate; /** * Servlet responsible for exposing the various {@link LoginProvider}s to the end user, as a way for him to login into the * application. * * This servlet shows to the user a login page that shows him a login form (if local login is enabled), or the option to choose an * alternative provider with which he can log in. * * Additionally, this servlet supports specifying a callback URL, to which the user * should be redirected after authentication is successful. For security purposes, * this URL *MUST* start with the configured application URL. * * @author João Carvalho (joao.pedro.carvalho@tecnico.ulisboa.pt) * */ @WebServlet("/login/*") public class PortalLoginServlet extends HttpServlet { private static final long serialVersionUID = -4298321185506045304L; private PebbleEngine engine; @Override public void init(ServletConfig config) throws ServletException { final ServletContext context = config.getServletContext(); this.engine = new Builder().extension(new PortalExtension(context)).loader(new ClasspathLoader() { @Override public Reader getReader(String themeName) throws LoaderException { // Try to resolve the page from the theme... InputStream stream = context.getResourceAsStream("/themes/" + themeName + "/login.html"); if (stream != null) { return new InputStreamReader(stream, StandardCharsets.UTF_8); } else { // ... and fall back if none is provided. return new InputStreamReader(context.getResourceAsStream("/bennu-portal/login.html"), StandardCharsets.UTF_8); } } }).cacheActive(!BennuPortalConfiguration.getConfiguration().themeDevelopmentMode()).build(); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String callback = req.getParameter("callback"); if (!validateCallback(callback)) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid callback URL"); return; } boolean localLogin = CoreConfiguration.getConfiguration().localLoginEnabled(); Collection<LoginProvider> providers = providers(); // If there is only one login option, show it right away if (!localLogin && providers.size() == 1) { providers.iterator().next().showLogin(req, resp, callback); return; } Map<String, Object> ctx = new HashMap<>(); PortalConfiguration config = PortalConfiguration.getInstance(); // Add relevant variables ctx.put("config", config); ctx.put("callback", callback); ctx.put("url", req.getRequestURI()); ctx.put("currentLocale", I18N.getLocale()); ctx.put("contextPath", req.getContextPath()); ctx.put("locales", CoreConfiguration.supportedLocales()); ctx.put("providers", providers); ctx.put("localLogin", localLogin); try { resp.setContentType("text/html;charset=UTF-8"); PebbleTemplate template = engine.getTemplate(config.getTheme()); template.evaluate(resp.getWriter(), ctx, I18N.getLocale()); } catch (PebbleException e) { throw new IOException(e); } } /** * Validates that the provided callback is valid, i.e., it is either not provided, or is a URL internal to the application. * * @param callback * The callback to validate. May be {@code null} * @return * Whether the provided callback is valid */ public static boolean validateCallback(String callback) { return Strings.isNullOrEmpty(callback) || callback.startsWith(CoreConfiguration.getConfiguration().applicationUrl()); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String callback = req.getParameter("callback"); if (!validateCallback(callback)) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid callback URL"); return; } LoginProvider provider = providerFor(req.getPathInfo()); if (provider == null) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unrecognized Login Provider"); } else { provider.showLogin(req, resp, callback); } } private LoginProvider providerFor(String pathInfo) { return pathInfo == null ? null : providers.get(pathInfo.replaceFirst("/", "")); } @Override public void destroy() { engine = null; } private static final ConcurrentMap<String, LoginProvider> providers = new ConcurrentHashMap<>(); /** * Registers the given provider. * * @param provider * The provider to register * @throws NullPointerException * If the given provider is {@code null} * @throws IllegalArgumentException * If another provider with the same key is already registered */ public static void registerProvider(LoginProvider provider) { if (providers.containsKey(provider.getKey())) { throw new IllegalArgumentException("Another provider with key " + provider.getKey() + " already exists"); } providers.put(provider.getKey(), provider); } private static Collection<LoginProvider> providers() { return providers.values().stream().filter(LoginProvider::isEnabled).collect(Collectors.toList()); } }