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