001    package edu.nrao.sss.webapp;
002    
003    import javax.servlet.*;
004    import javax.servlet.http.*;
005    import java.io.*;
006    import java.net.URLEncoder;
007    import java.util.*;
008    import javax.xml.xpath.XPathExpressionException;
009    import org.xml.sax.SAXException;
010    import javax.xml.parsers.ParserConfigurationException;
011    
012    import org.apache.log4j.Logger;
013    
014    /**
015      * AuthFilter redirects users to login with the User DB before proceeding with
016      * their query (if they haven't logged in yet).
017      * <p>
018      * When a request is received, the session is retrieved or created if it
019      * doesn't exist. If there is a edu.nrao.sss.webapp.User object in the session
020      * that has a name equal to {@link #userAttributeName}, then our query is
021      * passed through unaltered. If the user object doesn't exist, the filter looks
022      * to see if there is a paramter in the request query equal to {@link #AUTH_TOKEN}.
023      * If so, we create a user from the authentication token provided and store it
024      * in the session. If the authentication token is invalid, an exception is thrown.
025      * <p>
026      * If AUTH_TOKEN was not found in the request parameters and there is no user
027      * object, then the user needs to log in. We redirect to
028      * the address specified in {@link #loginPage}. 
029      * <p>
030      * The only exception to the behavior described above is when the original request
031      * parameters include a parameter that ends with "LOGOUT", in which case the current
032      * session is ivalidated (as are the cookies) and the user is redirected to the
033      * user db again with an address of <code>userDB + "?action=returnUser&tok=" + 
034      * authenticationToken</code>.
035      *
036      * If an extraneous or unnecessary AUTH_TOKEN query parameter is in the url,
037      * it is removed, and the user is redirected to remove the token from his/her
038      * browser url.
039      */
040    public class AuthFilter implements Filter
041    {
042            private static final Logger log = Logger.getLogger(AuthFilter.class);
043            
044            private static final Logger loginLog = Logger.getLogger("AuthFilter.Login");
045            
046            /** Name of the Fitler param used in web.xml to specify the userdb url*/
047            private static final String USER_DB = "UserDbUrl";
048    
049            /** The base url name for the user data base. The default is
050              * "http://webtest.aoc.nrao.edu/userdb"
051              */
052            protected String userDB;
053    
054    
055    
056            /** Name of the Fitler param used in web.xml to specify the attribute name
057              * in the session object that the User object is stored under.
058              *
059              * @see User
060              */
061            private static final String SESSION_USER = "UserAttributeName";
062    
063            /** The name of the attribute that will be put in the session that holds the
064              * User object. The default is "sessionUser"
065              *
066              * @see User
067              */
068            protected static String userAttributeName;
069    
070    
071            /** The name of the Session attribute holding a copy of the request query parameters. */
072            private static final String QUERY_CACHE_NAME = "RequestQueryCache";
073    
074            /** The name of the query parameter to retrieve the auth token. */
075            protected static final String AUTH_TOKEN =  "authenticationToken";
076    
077            /**
078              * set equal to this.userDB + "?action=login&returnAddress="
079              */
080            protected String loginPage;
081            
082            /**
083              * This method initializes the filter from param values in the web.xml
084              * config file.
085              *
086              * @see Filter#init
087              */
088            public void init(FilterConfig conf) throws ServletException
089            {
090                    ServletContext ctx = conf.getServletContext();
091                    this.userDB = conf.getInitParameter(USER_DB);
092                    if (this.userDB == null) 
093                    {
094                            //Try to find it in the ServletContext.
095                            this.userDB = ctx.getInitParameter(USER_DB);
096    
097                            //Assign a default if still not found.
098                            if (this.userDB == null) 
099                                    this.userDB = "http://webtest.aoc.nrao.edu/userdb";
100                    }
101    
102                    this.loginPage = this.userDB + "?action=login&returnAddress=";
103    
104                    userAttributeName = conf.getInitParameter(SESSION_USER);
105                    if (userAttributeName == null)
106                    {
107                            //Try to find it in the ServletContext.
108                            userAttributeName = ctx.getInitParameter(SESSION_USER);
109    
110                            //Assign a default if still not found.
111                            if (userAttributeName == null)
112                                    userAttributeName = "sessionUser";
113                    }
114            }
115    
116            /**
117             * @return the name of the HttpSession attribute that the User object is
118             * stored under.
119             */
120            public static String getUserAttributeName()
121            {
122                    return userAttributeName;
123            }
124    
125            /**
126              * This method checks to see if request has a valid session and it has a
127              * User object in the session. If not, it redirects the user to the userDB.
128              * @see #AuthFilter
129              * @see Filter#doFilter
130              */
131            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
132                    throws IOException, ServletException
133            {
134                    HttpServletRequest req = (HttpServletRequest)request;
135                    HttpServletResponse res = (HttpServletResponse)response;
136    
137                    //This shortcut is added here at the top to check if the user is logging out.
138                    for (Enumeration e = req.getParameterNames(); e.hasMoreElements();)
139                    {
140                            String name = (String)e.nextElement();
141                            if (name.endsWith("LOGOUT"))
142                            {
143                                    HttpSession s = req.getSession(false);
144    
145                                    String udb = this.userDB;
146                                    if (s != null)
147                                    {
148                                            User u = (User)s.getAttribute(userAttributeName);
149    
150                                            s.invalidate();
151    
152                                            Cookie[] cookies = req.getCookies();
153                                            if (cookies != null)
154                                            {
155                                                    for (Cookie cookie : cookies)
156                                                            cookie.setMaxAge(0);
157                                            }
158    
159                                            if (u != null)
160                                            {
161                                                    String tok = u.getAuthToken();
162    
163                                                    if (tok != null && tok.length() > 0)
164                                                    {
165                                                            udb = this.userDB + "?action=returnUser&tok=" + URLEncoder.encode(tok, "UTF-8");
166                                                    }
167    
168                                                    loginLog.info(
169                                                                    "(SessionId: " + s.getId() + ") " +
170                                                                    "User " + u.getUserId() + " has logged out."
171                                                                    );
172                                            }
173                                    }
174    
175                                    String url = res.encodeRedirectURL(udb);
176    
177                                    res.sendRedirect(url);
178                                    return;
179                            }
180                    }
181                    
182                    //Check to see if we have a session at all. If not, create one.
183                    HttpSession session = req.getSession(true);
184    
185                    //Check if we have an object in the session called
186                    //userAttributeName
187                    User user;
188    
189                    try
190                    {
191                            user = (User)session.getAttribute(userAttributeName);
192                    }
193                    catch(ClassCastException cce)
194                    {
195                            user = null;
196                    }
197    
198                    if (user == null)
199                    {
200                            //User needs to either log in or finish our login process by getting
201                            //user's XML from userdb.
202    
203                            String authTok = req.getParameter(AUTH_TOKEN);
204                            if (authTok == null)
205                            {
206                                    //The user hasn't logged in yet.
207                                    log.debug("User needs to log in, redirecting.");
208    
209                                    //Need to save query params here so we can put them back in the url
210                                    //after login.
211                                    cacheQueryInSession(req);
212    
213                                    redirectToLogin(req, res);
214                            }
215    
216                            else
217                            {
218                                    log.debug("User has authToken, but we need to rebuild url & redirect to final destination.");
219                                    
220                                    //The user already logged in, but we haven't built a user object
221                                    //from the xml query yet.
222                                    try
223                                    {
224                                            user = new User(authTok, this.userDB);
225                                            //put our user in the session
226                                            session.setAttribute(userAttributeName, user);
227                                            
228                                            StringBuffer loc = req.getRequestURL();
229                                            
230                                            //Need to fetch query params and add to loc here.
231                                            boolean addedParams = false;
232                                            Object params = session.getAttribute(QUERY_CACHE_NAME);
233    
234                                            if (params != null && params instanceof Map)
235                                            {
236                                                    String query = createQueryString((Map)params);
237                                                    if (query.length() > 0)
238                                                    {
239                                                            addedParams = true;
240                                                            loc.append(query);
241                                                    }
242                                                    session.removeAttribute(QUERY_CACHE_NAME);
243                                            }
244    
245                                            //Make sure any parameters that were passed besides AUTH_TOKEN
246                                            //are also included in the url.
247                                            String query = createQueryString(req.getParameterMap());
248    
249                                            //If we've already added a query string, replace the '?' of this
250                                            //query string with an '&'
251                                            if (addedParams)
252                                                    query = query.replaceFirst("\\?", "&");
253    
254                                            loc.append(query);
255    
256                                            loginLog.info(
257                                                    "(SessionId: " + session.getId() + ") " +
258                                                    "User " + user.getUserId() + " has logged in to access '" + loc + "'."
259                                            );
260                                            
261                                            res.sendRedirect(res.encodeRedirectURL(loc.toString()));
262                                    } catch (    XPathExpressionException e) { throw new ServletException(e);
263                                    } catch (                SAXException e) { throw new ServletException(e);
264                                    } catch (                 IOException e) { throw new ServletException(e);
265                                    } catch (ParserConfigurationException e) { throw new ServletException(e);
266                                    }
267                            }
268                    }
269    
270                    else
271                    {
272                            String authTok = req.getParameter(AUTH_TOKEN);
273                            if (authTok == null)
274                            {
275                                    log.debug("User has already logged in, granting access.");
276    
277                                    //Just forward the request on.
278                                    chain.doFilter(req, res);
279                            }
280    
281                            else
282                            {
283                                    //redirect to clear authTok from the query string. Make sure we
284                                    //keep all other query params in the query string.
285                                    StringBuffer loc = req.getRequestURL();
286    
287                                    String query = createQueryString(req.getParameterMap());
288                                    loc.append(query);
289    
290                                    res.sendRedirect(res.encodeRedirectURL(loc.toString()));
291                            }
292                    }
293            }
294    
295            /**
296              * Forwards to the user db appending an appropriately encoded return address.
297              */
298            private void redirectToLogin(HttpServletRequest req, HttpServletResponse res)
299                    throws IOException, ServletException
300            {
301                    StringBuffer retAddr = req.getRequestURL();
302                    String encodedRetAddr = java.net.URLEncoder.encode(retAddr.toString(), "UTF-8");
303    
304                    res.sendRedirect(
305                            res.encodeRedirectURL(this.loginPage + encodedRetAddr)
306                    );
307            }
308    
309            /**
310             * Caches all query parameters in the HttpSession associated with this
311             * request for later retrieval.
312             */
313            private void cacheQueryInSession(HttpServletRequest req)
314            {
315                    HttpSession s = req.getSession(true);
316                    s.setAttribute(QUERY_CACHE_NAME, req.getParameterMap());
317            }
318    
319            /**
320             * Creates a query string from <code>queryParams</code>.
321             * @param queryParams A Map of query Parameters with String keys and String[] values.
322             * @return A url query string with all the values in queryParams EXCEPT
323             * {@link #AUTH_TOKEN}. If queryParams is empty, returns an empty string.
324             */
325            private String createQueryString(Map queryParams)
326                    throws IOException
327            {
328                    StringBuilder queryStr = new StringBuilder("?");
329                    for (Object key : queryParams.keySet())
330                    {
331                            //Don't put auth token back in.
332                            if (!AUTH_TOKEN.equals(key))
333                            {
334                                    String keyStr = (String)key;
335                                    String[] vals = (String[])queryParams.get(key);
336                                    for (String val : vals)
337                                    {
338                                            String encodedVal = URLEncoder.encode(val, "UTF-8");
339                                            queryStr.append(keyStr).append("=").append(encodedVal).append("&");
340                                    }
341                            }
342                    }
343    
344                    //If no parameters were added, return an empty string
345                    if (queryStr.length() == 1)
346                            return "";
347    
348                    else
349                    {
350                            //Remove the trailing "&"
351                            queryStr.deleteCharAt(queryStr.length() - 1);
352                            return queryStr.toString();
353                    }
354            }
355    
356            /**
357              * This method is currently empty
358              * @see Filter#destroy
359              */
360            public void destroy() {}
361    }