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