View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.user2.login;
20  
21  import java.io.IOException;
22  import java.nio.charset.StandardCharsets;
23  import java.util.ArrayList;
24  import java.util.Collections;
25  import java.util.List;
26  import java.util.StringTokenizer;
27  import java.util.stream.Collectors;
28  
29  import javax.xml.transform.TransformerException;
30  
31  import org.apache.logging.log4j.LogManager;
32  import org.apache.logging.log4j.Logger;
33  import org.jdom2.Document;
34  import org.jdom2.Element;
35  import org.mycore.common.MCRSessionMgr;
36  import org.mycore.common.MCRSystemUserInformation;
37  import org.mycore.common.MCRUserInformation;
38  import org.mycore.common.config.MCRConfiguration2;
39  import org.mycore.common.content.MCRJAXBContent;
40  import org.mycore.common.content.MCRJDOMContent;
41  import org.mycore.frontend.MCRFrontendUtil;
42  import org.mycore.frontend.servlets.MCRServlet;
43  import org.mycore.frontend.servlets.MCRServletJob;
44  import org.mycore.frontend.support.MCRLogin.InputField;
45  import org.mycore.services.i18n.MCRTranslation;
46  import org.mycore.user2.MCRRealm;
47  import org.mycore.user2.MCRRealmFactory;
48  import org.mycore.user2.MCRUser;
49  import org.mycore.user2.MCRUser2Constants;
50  import org.mycore.user2.MCRUserManager;
51  import org.xml.sax.SAXException;
52  
53  import jakarta.servlet.ServletException;
54  import jakarta.servlet.http.HttpServletRequest;
55  import jakarta.servlet.http.HttpServletResponse;
56  import jakarta.xml.bind.JAXBContext;
57  import jakarta.xml.bind.JAXBException;
58  
59  /**
60   * Provides functionality to select login method,
61   * change login user and show a welcome page.
62   * Login methods and realms are configured in realms.xml.
63   * The login form for local users is login.xml. 
64   * 
65   * @author Frank L\u00fctzenkirchen
66   * @author Thomas Scheffler (yagee)
67   */
68  public class MCRLoginServlet extends MCRServlet {
69      protected static final String REALM_URL_PARAMETER = "realm";
70  
71      static final String HTTPS_ONLY_PROPERTY = MCRUser2Constants.CONFIG_PREFIX + "LoginHttpsOnly";
72  
73      static final String ALLOWED_ROLES_PROPERTY = MCRUser2Constants.CONFIG_PREFIX + "LoginAllowedRoles";
74  
75      private static final long serialVersionUID = 1L;
76  
77      private static final String LOGIN_REDIRECT_URL_PARAMETER = "url";
78  
79      private static final String LOGIN_REDIRECT_URL_KEY = "loginRedirectURL";
80  
81      protected static final boolean LOCAL_LOGIN_SECURE_ONLY = MCRConfiguration2
82          .getOrThrow(HTTPS_ONLY_PROPERTY, Boolean::parseBoolean);
83  
84      private static final List<String> ALLOWED_ROLES = MCRConfiguration2
85          .getString(MCRLoginServlet.ALLOWED_ROLES_PROPERTY)
86          .map(MCRConfiguration2::splitValue)
87          .map(s -> s.collect(Collectors.toList()))
88          .orElse(Collections.emptyList());
89  
90      private static Logger LOGGER = LogManager.getLogger();
91  
92      @Override
93      public void init() throws ServletException {
94          if (!LOCAL_LOGIN_SECURE_ONLY) {
95              LOGGER.warn("Login over unsecure connection is permitted. Set '" + HTTPS_ONLY_PROPERTY
96                  + "=true' to prevent cleartext transmissions of passwords.");
97          }
98          super.init();
99      }
100 
101     /**
102      * MCRLoginServlet handles four actions:
103      * 
104      * MCRLoginServlet?url=foo
105      * stores foo as redirect url and displays
106      * a list of login method options.
107      * 
108      * MCRLoginServlet?url=foo&amp;realm=ID
109      * stores foo as redirect url and redirects
110      * to the login URL of the given realm.
111     
112      * MCRLoginServlet?action=login
113      * checks input from editor login form and
114      * changes the current login user and redirects
115      * to the stored url.
116      * 
117      * MCRLoginServlet?action=cancel
118      * does not change login user, just
119      * redirects to the target url
120      */
121     public void doGetPost(MCRServletJob job) throws Exception {
122         HttpServletRequest req = job.getRequest();
123         HttpServletResponse res = job.getResponse();
124 
125         String action = req.getParameter("action");
126         String realm = req.getParameter(REALM_URL_PARAMETER);
127         job.getResponse().setHeader("Cache-Control", "no-cache");
128         job.getResponse().setHeader("Pragma", "no-cache");
129         job.getResponse().setHeader("Expires", "0");
130 
131         if ("login".equals(action)) {
132             presentLoginForm(job);
133         } else if ("cancel".equals(action)) {
134             redirect(res);
135         } else if (realm != null) {
136             loginToRealm(req, res, req.getParameter(REALM_URL_PARAMETER));
137         } else {
138             chooseLoginMethod(req, res);
139         }
140     }
141 
142     /**
143      * Stores the target url and outputs a list of realms to login to. The list is
144      * rendered using realms.xsl.
145      */
146     private void chooseLoginMethod(HttpServletRequest req, HttpServletResponse res) throws Exception {
147         storeURL(getReturnURL(req));
148         // redirect directly to login url if there is only one realm available and the user is not logged in
149         if ((getNumLoginOptions() == 1) && currentUserIsGuest()) {
150             redirectToUniqueRealm(req, res);
151         } else {
152             listRealms(req, res);
153         }
154     }
155 
156     protected static String getReturnURL(HttpServletRequest req) {
157         String returnURL = req.getParameter(LOGIN_REDIRECT_URL_PARAMETER);
158         if (returnURL == null) {
159             String referer = req.getHeader("Referer");
160             returnURL = (referer != null) ? referer : req.getContextPath() + "/";
161         }
162         return returnURL;
163     }
164 
165     private void redirectToUniqueRealm(HttpServletRequest req, HttpServletResponse res) throws Exception {
166         String realmID = MCRRealmFactory.listRealms().iterator().next().getID();
167         loginToRealm(req, res, realmID);
168     }
169 
170     protected void presentLoginForm(MCRServletJob job)
171         throws IOException, TransformerException, SAXException, JAXBException {
172         HttpServletRequest req = job.getRequest();
173         HttpServletResponse res = job.getResponse();
174         if (LOCAL_LOGIN_SECURE_ONLY && !req.isSecure()) {
175             res.sendError(HttpServletResponse.SC_FORBIDDEN, getErrorI18N("component.user2.login", "httpsOnly"));
176             return;
177         }
178 
179         String returnURL = getReturnURL(req);
180         String formAction = req.getRequestURI();
181         MCRLogin loginForm = new MCRLogin(MCRSessionMgr.getCurrentSession().getUserInformation(), returnURL,
182             formAction);
183         String uid = getProperty(req, "uid");
184         String pwd = getProperty(req, "pwd");
185         if (uid != null) {
186             MCRUser user = MCRUserManager.login(uid, pwd, ALLOWED_ROLES);
187             if (user == null) {
188                 res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
189                 loginForm.setLoginFailed(true);
190             } else {
191                 //user logged in
192                 // MCR-1154
193                 req.changeSessionId();
194                 LOGGER.info("user {} logged in successfully.", uid);
195                 res.sendRedirect(res.encodeRedirectURL(getReturnURL(req)));
196                 return;
197             }
198         }
199         addFormFields(loginForm, job.getRequest().getParameter(REALM_URL_PARAMETER));
200         getLayoutService().doLayout(req, res, new MCRJAXBContent<>(JAXBContext.newInstance(MCRLogin.class), loginForm));
201     }
202 
203     private void listRealms(HttpServletRequest req, HttpServletResponse res)
204         throws IOException, TransformerException, SAXException {
205         String redirectURL = getReturnURL(req);
206         Document realmsDoc = MCRRealmFactory.getRealmsDocument();
207         Element realms = realmsDoc.getRootElement();
208         addCurrentUserInfo(realms);
209         List<Element> realmList = realms.getChildren(REALM_URL_PARAMETER);
210         for (Element realm : realmList) {
211             String realmID = realm.getAttributeValue("id");
212             Element login = realm.getChild("login");
213             if (login != null) {
214                 login.setAttribute("url", MCRRealmFactory.getRealm(realmID).getLoginURL(redirectURL));
215             }
216         }
217         getLayoutService().doLayout(req, res, new MCRJDOMContent(realmsDoc));
218     }
219 
220     protected static void addFormFields(MCRLogin login, String loginToRealm) {
221         ArrayList<org.mycore.frontend.support.MCRLogin.InputField> fields = new ArrayList<>();
222         if (loginToRealm != null) {
223             //realmParameter
224             MCRRealm realm = MCRRealmFactory.getRealm(loginToRealm);
225             InputField realmParameter = new InputField(realm.getRealmParameter(), loginToRealm, null, null, false,
226                 true);
227             fields.add(realmParameter);
228         }
229         fields.add(new InputField("action", "login", null, null, false, true));
230         fields.add(new InputField("url", login.getReturnURL(), null, null, false, true));
231         String userNameText = MCRTranslation.translate("component.user2.login.form.userName");
232         fields.add(new InputField("uid", null, userNameText, userNameText, false, false));
233         String pwdText = MCRTranslation.translate("component.user2.login.form.password");
234         fields.add(new InputField("pwd", null, pwdText, pwdText, true, false));
235         login.getForm().getInput().addAll(fields);
236     }
237 
238     static void addCurrentUserInfo(Element rootElement) {
239         MCRUserInformation userInfo = MCRSessionMgr.getCurrentSession().getUserInformation();
240         rootElement.setAttribute("user", userInfo.getUserID());
241         String realmId = (userInfo instanceof MCRUser) ? ((MCRUser) userInfo).getRealm().getLabel()
242             : userInfo.getUserAttribute(MCRRealm.USER_INFORMATION_ATTR);
243         if (realmId == null) {
244             realmId = MCRRealmFactory.getLocalRealm().getLabel();
245         }
246         rootElement.setAttribute(REALM_URL_PARAMETER, realmId);
247         rootElement.setAttribute("guest", String.valueOf(currentUserIsGuest()));
248     }
249 
250     static void addCurrentUserInfo(MCRLogin login) {
251         MCRUserInformation userInfo = MCRSessionMgr.getCurrentSession().getUserInformation();
252         String realmId = (userInfo instanceof MCRUser) ? ((MCRUser) userInfo).getRealm().getLabel()
253             : userInfo.getUserAttribute(MCRRealm.USER_INFORMATION_ATTR);
254         if (realmId == null) {
255             realmId = MCRRealmFactory.getLocalRealm().getLabel();
256         }
257         login.setRealm(realmId);
258     }
259 
260     private static boolean currentUserIsGuest() {
261         return MCRSessionMgr.getCurrentSession().getUserInformation().getUserID()
262             .equals(MCRSystemUserInformation.getGuestInstance().getUserID());
263     }
264 
265     private int getNumLoginOptions() {
266         int numOptions = 0;
267         for (MCRRealm realm : MCRRealmFactory.listRealms()) {
268             numOptions++;
269             if (realm.getCreateURL() != null) {
270                 numOptions++;
271             }
272         }
273         return numOptions;
274     }
275 
276     private void loginToRealm(HttpServletRequest req, HttpServletResponse res, String realmID) throws Exception {
277         String redirectURL = getReturnURL(req);
278         storeURL(redirectURL);
279         MCRRealm realm = MCRRealmFactory.getRealm(realmID);
280         String loginURL = realm.getLoginURL(redirectURL);
281         res.sendRedirect(res.encodeRedirectURL(loginURL));
282     }
283 
284     /**
285      * Stores the given url in MCRSession. When login is canceled, or after
286      * successful login, the browser is redirected to that url. 
287      */
288     private void storeURL(String url) throws Exception {
289         if ((url == null) || (url.trim().length() == 0)) {
290             url = MCRFrontendUtil.getBaseURL();
291         } else if (url.startsWith(MCRFrontendUtil.getBaseURL()) && !url.equals(MCRFrontendUtil.getBaseURL())) {
292             String rest = url.substring(MCRFrontendUtil.getBaseURL().length());
293             url = MCRFrontendUtil.getBaseURL() + encodePath(rest);
294         }
295         LOGGER.info("Storing redirect URL to session: {}", url);
296         MCRSessionMgr.getCurrentSession().put(LOGIN_REDIRECT_URL_KEY, url);
297     }
298 
299     private String encodePath(String path) throws Exception {
300         path = path.replace('\\', '/');
301 
302         StringBuilder result = new StringBuilder();
303         StringTokenizer st = new StringTokenizer(path, " /?&=", true);
304 
305         while (st.hasMoreTokens()) {
306             String token = st.nextToken();
307             switch (token) {
308             case " ":
309                 result.append("%20");
310                 break;
311             case "/":
312             case "?":
313             case "&":
314             case "=":
315                 result.append(token);
316                 break;
317             default:
318                 result.append(java.net.URLEncoder.encode(token, StandardCharsets.UTF_8));
319                 break;
320             }
321         }
322 
323         return result.toString();
324     }
325 
326     /**
327      * Redirects the browser to the target url.
328      */
329     static void redirect(HttpServletResponse res) throws Exception {
330         String url = (String) (MCRSessionMgr.getCurrentSession().get(LOGIN_REDIRECT_URL_KEY));
331         if (url == null) {
332             LOGGER.warn("Could not get redirect URL from session.");
333             url = MCRFrontendUtil.getBaseURL();
334         }
335         LOGGER.info("Redirecting to url: {}", url);
336         res.sendRedirect(res.encodeRedirectURL(url));
337     }
338 }