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 */
019package org.apache.wiki.auth.login;
020
021import java.io.*;
022import java.util.UUID;
023
024import javax.security.auth.callback.Callback;
025import javax.security.auth.callback.UnsupportedCallbackException;
026import javax.security.auth.login.LoginException;
027import javax.servlet.http.Cookie;
028import javax.servlet.http.HttpServletRequest;
029import javax.servlet.http.HttpServletResponse;
030
031import org.apache.log4j.Logger;
032import org.apache.wiki.WikiEngine;
033import org.apache.wiki.auth.WikiPrincipal;
034import org.apache.wiki.util.FileUtil;
035import org.apache.wiki.util.HttpUtil;
036import 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 */
065public 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}