001    /*
002        Licensed to the Apache Software Foundation (ASF) under one
003        or more contributor license agreements.  See the NOTICE file
004        distributed with this work for additional information
005        regarding copyright ownership.  The ASF licenses this file
006        to you under the Apache License, Version 2.0 (the
007        "License"); you may not use this file except in compliance
008        with the License.  You may obtain a copy of the License at
009    
010           http://www.apache.org/licenses/LICENSE-2.0
011    
012        Unless required by applicable law or agreed to in writing,
013        software distributed under the License is distributed on an
014        "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015        KIND, either express or implied.  See the License for the
016        specific language governing permissions and limitations
017        under the License.    
018     */
019    package org.apache.wiki.auth.login;
020    
021    import java.io.*;
022    import java.util.UUID;
023    
024    import javax.security.auth.callback.Callback;
025    import javax.security.auth.callback.UnsupportedCallbackException;
026    import javax.security.auth.login.LoginException;
027    import javax.servlet.http.Cookie;
028    import javax.servlet.http.HttpServletRequest;
029    import javax.servlet.http.HttpServletResponse;
030    
031    import org.apache.log4j.Logger;
032    import org.apache.wiki.WikiEngine;
033    import org.apache.wiki.auth.WikiPrincipal;
034    import org.apache.wiki.util.FileUtil;
035    import org.apache.wiki.util.HttpUtil;
036    import org.apache.wiki.util.TextUtil;
037    
038    /**
039     *  Logs in an user based on a cookie stored in the user's computer.  The cookie
040     *  information is stored in the <code>jspwiki.workDir</code>, under the directory
041     *  {@value #COOKIE_DIR}.  For security purposes it is a very, very good idea
042     *  to prevent access to this directory by everyone except the web server process;
043     *  otherwise people having read access to this directory may be able to spoof
044     *  other users.
045     *  <p>
046     *  The cookie directory is scrubbed of old entries at regular intervals.
047     *  <p>
048     *   This module must be used with a CallbackHandler (such as
049     *   {@link WebContainerCallbackHandler}) that supports the following Callback
050     *   types:
051     *   </p>
052     *   <ol>
053     *   <li>{@link HttpRequestCallback}- supplies the cookie, which should contain
054     *       an unique id for fetching the UID.</li>
055     *   <li>{@link WikiEngineCallback} - allows access to the WikiEngine itself.
056     *   </ol>
057     *  <p>
058     *  After authentication, a generic WikiPrincipal based on the username will be
059     *  created and associated with the Subject.
060     *  </p>
061     *  @see javax.security.auth.spi.LoginModule#commit()
062     *  @see CookieAssertionLoginModule
063     *  @since  2.5.62
064     */
065    public class CookieAuthenticationLoginModule extends AbstractLoginModule
066    {
067    
068        private static final Logger log = Logger.getLogger( CookieAuthenticationLoginModule.class );
069        private static final String LOGIN_COOKIE_NAME = "JSPWikiUID";
070    
071        /** The directory name under which the cookies are stored.  The value is {@value}. */
072        protected static final String COOKIE_DIR        = "logincookies";
073    
074        /**
075         *  User property for setting how long the cookie is stored on the user's computer.
076         *  The value is {@value}.  The default expiry time is 14 days.
077         */
078        public static final  String PROP_LOGIN_EXPIRY_DAYS  = "jspwiki.cookieAuthentication.expiry";
079    
080        /**
081         *  Built-in value for storing the cookie.
082         */
083        private static final int    DEFAULT_EXPIRY_DAYS = 14;
084    
085        private static       long   c_lastScrubTime   = 0L;
086    
087        /** Describes how often we scrub the cookieDir directory.
088         */
089        private static final long   SCRUB_PERIOD      = 60*60*1000L; // In milliseconds
090    
091        /**
092         * @see javax.security.auth.spi.LoginModule#login()
093         * 
094         * {@inheritDoc}
095         */
096        public boolean login() throws LoginException
097        {
098            // Otherwise, let's go and look for the cookie!
099            HttpRequestCallback hcb = new HttpRequestCallback();
100            //UserDatabaseCallback ucb = new UserDatabaseCallback();
101            WikiEngineCallback wcb  = new WikiEngineCallback();
102    
103            Callback[] callbacks = new Callback[]
104            { hcb, wcb };
105    
106            try
107            {
108                m_handler.handle( callbacks );
109    
110                HttpServletRequest request = hcb.getRequest();
111                String uid = getLoginCookie( request );
112    
113                if( uid != null )
114                {
115                    WikiEngine engine = wcb.getEngine();
116                    File cookieFile = getCookieFile(engine, uid);
117    
118                    if( cookieFile != null && cookieFile.exists() && cookieFile.canRead() )
119                    {
120                        Reader in = null;
121    
122                        try
123                        {
124                            in = new BufferedReader( new InputStreamReader( new FileInputStream( cookieFile ), "UTF-8" ) );
125                            String username = FileUtil.readContents( in );
126    
127                            if ( log.isDebugEnabled() )
128                            {
129                                log.debug( "Logged in cookie authenticated name=" + username );
130                            }
131    
132                            // If login succeeds, commit these principals/roles
133                            m_principals.add( new WikiPrincipal( username,  WikiPrincipal.LOGIN_NAME ) );
134    
135                            //
136                            //  Tag the file so that we know that it has been accessed recently.
137                            //
138                            cookieFile.setLastModified( System.currentTimeMillis() );
139    
140                            return true;
141                        }
142                        catch( IOException e )
143                        {
144                            return false;
145                        }
146                        finally
147                        {
148                            if( in != null ) in.close();
149                        }
150                    }
151                }
152            }
153            catch( IOException e )
154            {
155                String message = "IO exception; disallowing login.";
156                log.error( message, e );
157                throw new LoginException( message );
158            }
159            catch( UnsupportedCallbackException e )
160            {
161                String message = "Unable to handle callback; disallowing login.";
162                log.error( message, e );
163                throw new LoginException( message );
164            }
165    
166            return false;
167        }
168    
169        /**
170         *  Attempts to locate the cookie file.
171         *  @param engine WikiEngine
172         *  @param uid An unique ID fetched from the user cookie
173         *  @return A File handle, or null, if there was a problem.
174         */
175        private static File getCookieFile(WikiEngine engine, String uid)
176        {
177            File cookieDir = new File( engine.getWorkDir(), COOKIE_DIR );
178    
179            if( !cookieDir.exists() )
180            {
181                cookieDir.mkdirs();
182            }
183    
184            if( !cookieDir.canRead() )
185            {
186                log.error("Cannot read from cookie directory!"+cookieDir.getAbsolutePath());
187                return null;
188            }
189    
190            if( !cookieDir.canWrite() )
191            {
192                log.error("Cannot write to cookie directory!"+cookieDir.getAbsolutePath());
193                return null;
194            }
195    
196            //
197            //  Scrub away old files
198            //
199            long now = System.currentTimeMillis();
200    
201            if( now > (c_lastScrubTime+SCRUB_PERIOD ) )
202            {
203                scrub( TextUtil.getIntegerProperty( engine.getWikiProperties(),
204                                                    PROP_LOGIN_EXPIRY_DAYS,
205                                                    DEFAULT_EXPIRY_DAYS ),
206                                                    cookieDir );
207                c_lastScrubTime = now;
208            }
209    
210            //
211            //  Find the cookie file
212            //
213            File cookieFile = new File( cookieDir, uid );
214            return cookieFile;
215        }
216    
217        /**
218         *  Extracts the login cookie UID from the servlet request.
219         *
220         *  @param request The HttpServletRequest
221         *  @return The UID value from the cookie, or null, if no such cookie exists.
222         */
223        private static String getLoginCookie(HttpServletRequest request)
224        {
225            String cookie = HttpUtil.retrieveCookieValue( request, LOGIN_COOKIE_NAME );
226    
227            return cookie;
228        }
229    
230        /**
231         *  Sets a login cookie based on properties set by the user.  This method also
232         *  creates the cookie uid-username mapping in the work directory.
233         *
234         *  @param engine The WikiEngine
235         *  @param response The HttpServletResponse
236         *  @param username The username for whom to create the cookie.
237         */
238        // FIXME3.0: For 3.0, switch to using java.util.UUID so that we can
239        //           get rid of jug.jar
240        public static void setLoginCookie( WikiEngine engine, HttpServletResponse response, String username )
241        {
242            UUID uid = UUID.randomUUID();
243    
244            int days = TextUtil.getIntegerProperty( engine.getWikiProperties(),
245                                                    PROP_LOGIN_EXPIRY_DAYS,
246                                                    DEFAULT_EXPIRY_DAYS );
247    
248            Cookie userId = new Cookie( LOGIN_COOKIE_NAME, uid.toString() );
249            userId.setMaxAge( days * 24 * 60 * 60 );
250            response.addCookie( userId );
251    
252            File cf = getCookieFile( engine, uid.toString() );
253            Writer out = null;
254    
255            try
256            {
257                //
258                //  Write the cookie content to the cookie store file.
259                //
260                out = new BufferedWriter( new OutputStreamWriter( new FileOutputStream(cf), "UTF-8" ) );
261                FileUtil.copyContents( new StringReader(username), out );
262    
263                if( log.isDebugEnabled() )
264                {
265                    log.debug( "Created login cookie for user "+username+" for "+days+" days" );
266                }
267            }
268            catch( IOException ex )
269            {
270                log.error("Unable to create cookie file to store user id: "+uid);
271            }
272            finally
273            {
274                if( out != null )
275                {
276                    try
277                    {
278                        out.close();
279                    }
280                    catch( IOException ex )
281                    {
282                        log.error("Unable to close stream");
283                    }
284                }
285            }
286        }
287    
288        /**
289         *  Clears away the login cookie, and removes the uid-username mapping file as well.
290         *
291         *  @param engine   WikiEngine
292         *  @param request  Servlet request
293         *  @param response Servlet response
294         */
295        public static void clearLoginCookie( WikiEngine engine, HttpServletRequest request, HttpServletResponse response )
296        {
297            Cookie userId = new Cookie( LOGIN_COOKIE_NAME, "" );
298            userId.setMaxAge( 0 );
299            response.addCookie( userId );
300    
301            String uid = getLoginCookie( request );
302    
303            if( uid != null )
304            {
305                File cf = getCookieFile( engine, uid );
306    
307                if( cf != null )
308                {
309                    cf.delete();
310                }
311            }
312        }
313    
314        /**
315         *  Goes through the cookie directory and removes any obsolete files.
316         *  The scrubbing takes place one day after the cookie was supposed to expire.
317         *  However, if the user has logged in during the expiry period, the expiry is
318         *  reset, and the cookie file left here.
319         *
320         *  @param days
321         *  @param cookieDir
322         */
323        private static synchronized void scrub( int days, File cookieDir )
324        {
325            log.debug("Scrubbing cookieDir...");
326    
327            File[] files = cookieDir.listFiles();
328    
329            long obsoleteDateLimit = System.currentTimeMillis() - ((long)days+1) * 24 * 60 * 60 * 1000L;
330    
331            int  deleteCount = 0;
332    
333            for( int i = 0; i < files.length; i++ )
334            {
335                File f = files[i];
336    
337                long lastModified = f.lastModified();
338    
339                if( lastModified < obsoleteDateLimit )
340                {
341                    f.delete();
342                    deleteCount++;
343                }
344            }
345    
346            log.debug("Removed "+deleteCount+" obsolete cookie logins");
347        }
348    }