/* * Copyright (C) 2000 - 2008 TagServlet Ltd * * This file is part of Open BlueDragon (OpenBD) CFML Server Engine. * * OpenBD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * Free Software Foundation,version 3. * * OpenBD is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with OpenBD. If not, see http://www.gnu.org/licenses/ * * Additional permission under GNU GPL version 3 section 7 * * If you modify this Program, or any covered work, by linking or combining * it with any of the JARS listed in the README.txt (or a modified version of * (that library), containing parts covered by the terms of that JAR, the * licensors of this Program grant you additional permission to convey the * resulting work. * README.txt @ http://www.openbluedragon.org/license/README.txt * * http://www.openbluedragon.org/ */ package com.naryx.tagfusion.cfm.tag; import java.io.Serializable; import java.util.Enumeration; import java.util.Map; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import com.nary.net.Base64; import com.nary.security.SessionLoginToken; import com.nary.util.FastMap; import com.naryx.tagfusion.cfm.application.cfAPPLICATION; import com.naryx.tagfusion.cfm.application.cfApplicationData; import com.naryx.tagfusion.cfm.application.cfSessionData; import com.naryx.tagfusion.cfm.engine.cfData; import com.naryx.tagfusion.cfm.engine.cfFormData; import com.naryx.tagfusion.cfm.engine.cfSession; import com.naryx.tagfusion.cfm.engine.cfStructData; import com.naryx.tagfusion.cfm.engine.cfmBadFileException; import com.naryx.tagfusion.cfm.engine.cfmRunTimeException; import com.naryx.tagfusion.cfm.engine.variableStore; public class cfLOGIN extends cfTag implements Serializable { static final long serialVersionUID = 1; public static final String DATA_BIN_KEY = "CFLOGIN_DATA"; static final String ATT_NAME_IDLETIMEOUT = "IDLETIMEOUT"; static final String ATT_NAME_APPTOKEN = "APPLICATIONTOKEN"; static final String ATT_NAME_COOKIEDOMAIN = "COOKIEDOMAIN"; static final int DEFAULT_IDLE_TIMEOUT = 1800; /** * Testing with CFMX 6.1 revealed that if the application is not named (via * <cfapplication name="myApp">) Then the Login cookie is given the name * "CFAUTHORIZATION_" * * If it IS named... for example using <cfapplication name="myApp"> then * CFMX 6.1 will name the login cookie "CFAUTHORIZATION_myApp" * * CFMX 6.1 does not appear to honor/use the value of the appToken attribute * at all */ static final String DEFAULT_LOGIN_COOKIE_NAME = "CFAUTHORIZATION_"; /** * This is the key used to store the base64 encoded form of * "<username>:<password>:<applicationTokenValue>" in the session * With CFMX 6.1 the key is "cfauthorization" */ static final String DEFAULT_LOGIN_SESSION_ATTRIBUTE_NAME = "cfauthorization"; /** * set default values for the tag's attributes * * @param _tag * The String representation of the opening tag. * */ protected void defaultParameters(String _tag) throws cfmBadFileException { // set default values for all tag attributes whose default values we know at // this point defaultAttribute(ATT_NAME_IDLETIMEOUT, DEFAULT_IDLE_TIMEOUT); // now read in all name/value pairs for all attributes of the tag // (potentially overwriting default values) parseTagHeader(_tag); } /** * This method is used to perform further attribute defaulting, when the * cfSession object is needed in order to figure out the default value. * * @param _Session * @throws cfmRunTimeException */ private void defaultParametersAdvanced(cfSession _Session) { unclipAttributes(); if (!containsAttribute(ATT_NAME_APPTOKEN)) { // if we get here, the cflogin tag did not make use of the // applicationToken attribute String appToken = getAppName(_Session); defaultAttribute(ATT_NAME_APPTOKEN, appToken); } } public static String getAppName(cfSession _Session) { cfApplicationData appData = _Session.getApplicationData(); if (appData != null) return appData.getAppName(); else return cfAPPLICATION.UNNAMED_APPNAME; } /** * * @param _Session * @return the cflogin structure or null */ public static cfStructData extractUserPass(cfSession _Session) { HttpServletRequest req = _Session.REQ; boolean gotUsername = false; boolean gotPassword = false; String nameKey = "name"; String passKey = "password"; cfStructData userPass = new cfStructData(); // setup default values userPass.put(nameKey, ""); userPass.put(passKey, ""); // 1st look for the j_username and j_password request parameters (CFMX 6.1 // can get them for both POST and GET requests, so we should too) // do it in a way that the cAsE of the parameter names won't matter since // CFMX 6.1 does not care about case... (i.e. don't just use // req.getParameter() directly) // if the parameters were sent via a POST request on a form then we need to // look for them in the FORM scope, since they won't be in the request // parameters (BD wraps the request object and // does special logic in this case). cfFormData formData = (cfFormData) _Session.getQualifiedData(variableStore.FORM_SCOPE); if (formData != null && formData.size() > 0) { // cfformdata is not case-sensitive so we can just use lower-case here. if (formData.containsKey("j_username")) { gotUsername = true; userPass.put(nameKey, formData.getData("j_username")); } if (formData.containsKey("j_password")) { gotPassword = true; userPass.put(passKey, formData.getData("j_password")); } // got at least one if (gotUsername || gotPassword) return userPass; } // if we get here, we got neither so try to get username and password a // different way... // if the parameters were sent via a GET request (querystring on the URL) // then we need to look for them in the request parameters, since that's // where they // will be (i.e. they won't be in the FORM scope). Enumeration<String> enumer = req.getParameterNames(); while (enumer.hasMoreElements()) { String name = enumer.nextElement(); String lName = name.toLowerCase(); boolean isUsername = false; boolean isPassword = false; if ((isUsername = lName.equals("j_username")) || (isPassword = lName.equals("j_password"))) { String key = null; String val = req.getParameter(name); if (isUsername && !gotUsername) // only honor the first one sent { gotUsername = true; key = nameKey; } else if (isPassword && !gotPassword) // only honor the first one sent { gotPassword = true; key = passKey; } if (key != null) userPass.put(key, val); } if (gotUsername && gotPassword) break; } // got at least one if (gotUsername || gotPassword) return userPass; // if we get here, we got neither so try to get username and password a // different way... // 2nd see if the user/pass has been sent using BASIC Authentication String authHeader = req.getHeader("authorization"); if (authHeader != null && authHeader.trim().toUpperCase().startsWith("BASIC")) { StringBuilder username = new StringBuilder(); StringBuilder password = new StringBuilder(); if (usernamePassword(req, username, password)) { userPass.put(nameKey, username.toString()); userPass.put(passKey, password.toString()); return userPass; } } // if we get here we still have not gotten the username and password // 3rd see if the user/pass has been sent using NTLM Authentication /* * CFMX 6.1 livedocs say that in this case: "ColdFusion gets the username * from the web server and sets the cflogin.password value to the empty * string" */ else if (authHeader != null && authHeader.trim().toUpperCase().startsWith("NTLM")) { String usernameFromWebContainer = req.getRemoteUser(); if (usernameFromWebContainer == null) usernameFromWebContainer = ""; userPass.put(nameKey, usernameFromWebContainer); userPass.put(passKey, ""); return userPass; } // 4th see if the user/pass has been sent using DIGEST Authentication // not implemented at this time (SE 5.0 doesn't even implement this... // practically no one uses DIGEST auth) return null; } /** * This method will extract the username and password from the Authorization * request header, Decode them from their base-64 values and store them in the * username and password parameters * * @param req * @param username * @param password * @return true if username and password were extracted, else false is * returned */ public static boolean usernamePassword(HttpServletRequest req, StringBuilder username, StringBuilder password) { int pos; String auth = req.getHeader("authorization"); if (auth == null) return false; // Remove "Basic " from the beginning of the Authorization string pos = auth.indexOf(' '); if (pos == -1) return false; auth = auth.substring(pos + 1); String str = Base64.base64Decode(auth); pos = str.indexOf(':'); // If we found the colon then extract the username and password. if (pos > 0) { username.append(str.substring(0, pos)); password.append(str.substring(pos + 1)); return true; } return false; } /** * * @param _Session */ public cfTagReturnType render(cfSession _Session) throws cfmRunTimeException { if (!isUserLoggedIn(_Session)) { defaultParametersAdvanced(_Session); // get the username and password cfStructData userPass = extractUserPass(_Session); if (userPass != null) { // userPass.getData("name").getString(); _Session.setData("cflogin", userPass); // expose the cflogin struct } // communicate some information to the <cfloginuser> subtag Map<String, cfData> map = new FastMap<String, cfData>(); map.put(ATT_NAME_APPTOKEN, getDynamic(_Session, ATT_NAME_APPTOKEN)); map.put(ATT_NAME_IDLETIMEOUT, getDynamic(_Session, ATT_NAME_IDLETIMEOUT)); if (containsAttribute(cfLOGIN.ATT_NAME_COOKIEDOMAIN)) map.put(ATT_NAME_COOKIEDOMAIN, getDynamic(_Session, ATT_NAME_COOKIEDOMAIN)); _Session.setDataBin(DATA_BIN_KEY, map); // now any subtag will be able to see this information via // _Session.getDataBin(cdfLOGINUSER.DATA_BIN_KEY); super.render(_Session); if (userPass != null) _Session.deleteData("cflogin"); // remove the cflogin struct so that // it's not visible after the closing // tag } return cfTagReturnType.NORMAL; } /** * * @param appData * @return the value specified by the loginStorage attribute of the * application (<cfapplication>). This would be either "COOKIE" * (the default) or "SESSION". */ public static String getLoginStorageType(cfApplicationData appData) { if (appData != null) return appData.getLoginStorage(); else return cfAPPLICATION.DEFAULT_LOGIN_STORAGE; } /** * * @param appData * @return The name to use for the cookie whose value will be a base-64 * encoded form of * "<username>:<password>:<applicationTokenValue>" With CFMX * 6.1 this is: <br> * "CFAUTHORIZATION_" + <application name> * */ public static String getLoginCookieName(cfApplicationData appData) { if (appData != null) return DEFAULT_LOGIN_COOKIE_NAME + appData.getAppName(); else return DEFAULT_LOGIN_COOKIE_NAME; } /** * * @return the name to use for the session attribute whose value will be a * base-64 encoded form of * "<username>:<password>:<applicationTokenValue>" */ public static String getLoginSessionAttributeName() { return DEFAULT_LOGIN_SESSION_ATTRIBUTE_NAME; } /** * This method will search either the cookie scope or the session scope for * the value. * * @param _Session * @return The value of the login token, which will be * <username>:<password>:<applicationToken> (base64 encoded) * @throws cfmRunTimeException */ public static String getLoginTokenValue(cfSession _Session) { String loginTokenValue = null; cfApplicationData appData = _Session.getApplicationData(); // appData may be null String loginStorageType = getLoginStorageType(appData); // test for loginStorage=="session" if (cfAPPLICATION.ALT_LOGIN_STORAGE_1.equalsIgnoreCase(loginStorageType)) { // login token is/will-be an attribute in the session scope (which may be // a J2EE session scope or may be the CF session scope) cfSessionData session = (cfSessionData) _Session.getQualifiedData(variableStore.SESSION_SCOPE); if (session != null) { SessionLoginToken loginToken = (SessionLoginToken) session.getData(getLoginSessionAttributeName()); if (loginToken != null) loginTokenValue = loginToken.toString(); } } else // the default (login token is/will-be a cookie) { HttpServletRequest req = _Session.REQ; Cookie[] cookies = req.getCookies(); if (cookies != null) { String cookieName = getLoginCookieName(appData); for (int i = 0; i < cookies.length; i++) { Cookie cookie = cookies[i]; if (cookie.getName().equals(cookieName)) { loginTokenValue = cookie.getValue(); break; } } } } return loginTokenValue; } /** * * A user is logged in if the decoded loginTokenValue is found, and is found * to be the correct value. This would be the case if: * * A cookie with the right name is there, and the <applicationToken> * portion of its value is correct. OR A session attribute with the right name * is there, and the <applicationToken> portion of its value is correct. * * @param _Session * @return true if the user is already logged in, else false is returned. * @throws cfmRunTimeException */ public boolean isUserLoggedIn(cfSession _Session) throws cfmRunTimeException { boolean loggedIn = false; String loginTokenValue = null; // part of the fix for bug #2008 // 1st try getting the loginTokenValue this way, to cover the case that // <cfloginuser> just occured on the same page (or request) Object o = _Session.getDataBin(cfLOGINUSER.DATA_BIN_KEY); // this was set by // cfLOGINUSER.render() if (o != null) loginTokenValue = (String) o; else // 2nd check for a cookie or a session attribute loginTokenValue = getLoginTokenValue(_Session); if (loginTokenValue != null) { // make sure that there are some roles defined for the user... if not then // they are not logged in Map<String, String> data = _Session.getDataFromSecurityStore(loginTokenValue); if (data != null) { String loginTokenValueDecoded = Base64.base64Decode(loginTokenValue); // loginTokenValueDecoded now represents // "<username>:<password>:<applicationTokenValue>" // we need to ensure that <applicationTokenValue> matches the value of // the appToken attribute of the cflogin tag int lastColonPos = loginTokenValueDecoded.lastIndexOf(":"); if (lastColonPos > 0) { String storedAppTokenValue = loginTokenValueDecoded.substring(lastColonPos + 1); cfData appToken = getDynamic(_Session, ATT_NAME_APPTOKEN); if (appToken != null) // testing appToken for null here is the fix for // bug #1867 loggedIn = storedAppTokenValue.equals(appToken.toString()); else // part of the fix for bug #2008 loggedIn = storedAppTokenValue.equals(getAppName(_Session)); } } } return loggedIn; } public String getEndMarker() { return "</CFLOGIN>"; } }