    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;
    020
    021import org.apache.logging.log4j.LogManager;
    022import org.apache.logging.log4j.Logger;
    023import org.apache.wiki.api.core.Engine;
    024import org.apache.wiki.api.core.Session;
    025import org.apache.wiki.api.exceptions.WikiException;
    026import org.apache.wiki.api.spi.Wiki;
    027import org.apache.wiki.auth.authorize.WebAuthorizer;
    028import org.apache.wiki.auth.authorize.WebContainerAuthorizer;
    029import org.apache.wiki.auth.login.AnonymousLoginModule;
    030import org.apache.wiki.auth.login.CookieAssertionLoginModule;
    031import org.apache.wiki.auth.login.CookieAuthenticationLoginModule;
    032import org.apache.wiki.auth.login.UserDatabaseLoginModule;
    033import org.apache.wiki.auth.login.WebContainerCallbackHandler;
    034import org.apache.wiki.auth.login.WebContainerLoginModule;
    035import org.apache.wiki.auth.login.WikiCallbackHandler;
    036import org.apache.wiki.event.WikiEventListener;
    037import org.apache.wiki.event.WikiEventManager;
    038import org.apache.wiki.event.WikiSecurityEvent;
    039import org.apache.wiki.util.ClassUtil;
    040import org.apache.wiki.util.TextUtil;
    041import org.apache.wiki.util.TimedCounterList;
    042
    043import javax.security.auth.Subject;
    044import javax.security.auth.callback.CallbackHandler;
    045import javax.security.auth.login.LoginException;
    046import javax.security.auth.spi.LoginModule;
    047import javax.servlet.http.HttpServletRequest;
    048import javax.servlet.http.HttpSession;
    049import java.security.Principal;
    050import java.util.Collections;
    051import java.util.HashMap;
    052import java.util.HashSet;
    053import java.util.Map;
    054import java.util.Properties;
    055import java.util.Set;
    056
    057
    058/**
    059 * Default implementation for {@link AuthenticationManager}
    060 *
    061 * {@inheritDoc}
    062 *
    063 * @since 2.3
    064 */
    065public class DefaultAuthenticationManager implements AuthenticationManager {
    066
    067    /** How many milliseconds the logins are stored before they're cleaned away. */
    068    private static final long LASTLOGINS_CLEANUP_TIME = 10 * 60 * 1_000L; // Ten minutes
    069
    070    private static final long MAX_LOGIN_DELAY = 20 * 1_000L; // 20 seconds
    071
    072    private static final Logger LOG = LogManager.getLogger( DefaultAuthenticationManager.class );
    073
    074    /** Empty Map passed to JAAS {@link #doJAASLogin(Class, CallbackHandler, Map)} method. */
    075    protected static final Map< String, String > EMPTY_MAP = Collections.unmodifiableMap( new HashMap<>() );
    076
    077    /** Class (of type LoginModule) to use for custom authentication. */
    078    protected Class< ? extends LoginModule > m_loginModuleClass = UserDatabaseLoginModule.class;
    079
    080    /** Options passed to {@link LoginModule#initialize(Subject, CallbackHandler, Map, Map)};
    081     * initialized by {@link #initialize(Engine, Properties)}. */
    082    protected final Map< String, String > m_loginModuleOptions = new HashMap<>();
    083
    084    /** The default {@link LoginModule} class name to use for custom authentication. */
    085    private static final String DEFAULT_LOGIN_MODULE = "org.apache.wiki.auth.login.UserDatabaseLoginModule";
    086
    087    /** Empty principal set. */
    088    private static final Set<Principal> NO_PRINCIPALS = new HashSet<>();
    089
    090    /** Static Boolean for lazily-initializing the "allows assertions" flag */
    091    private boolean m_allowsCookieAssertions = true;
    092
    093    private boolean m_throttleLogins = true;
    094
    095    /** Static Boolean for lazily-initializing the "allows cookie authentication" flag */
    096    private boolean m_allowsCookieAuthentication;
    097
    098    private Engine m_engine;
    099
    100    /** If true, logs the IP address of the editor */
    101    private boolean m_storeIPAddress = true;
    102
    103    /** Keeps a list of the usernames who have attempted a login recently. */
    104    private final TimedCounterList< String > m_lastLoginAttempts = new TimedCounterList<>();
    105
    106    /**
    107     * {@inheritDoc}
    108     */
    109    @Override
    110    public void initialize( final Engine engine, final Properties props ) throws WikiException {
    111        m_engine = engine;
    112        m_storeIPAddress = TextUtil.getBooleanProperty( props, PROP_STOREIPADDRESS, m_storeIPAddress );
    113
    114        // Should we allow cookies for assertions? (default: yes)
    115        m_allowsCookieAssertions = TextUtil.getBooleanProperty( props, PROP_ALLOW_COOKIE_ASSERTIONS,true );
    116
    117        // Should we allow cookies for authentication? (default: no)
    118        m_allowsCookieAuthentication = TextUtil.getBooleanProperty( props, PROP_ALLOW_COOKIE_AUTH, false );
    119
    120        // Should we throttle logins? (default: yes)
    121        m_throttleLogins = TextUtil.getBooleanProperty( props, PROP_LOGIN_THROTTLING, true );
    122
    123        // Look up the LoginModule class
    124        final String loginModuleClassName = TextUtil.getStringProperty( props, PROP_LOGIN_MODULE, DEFAULT_LOGIN_MODULE );
    125        try {
    126            m_loginModuleClass = ClassUtil.findClass( "", loginModuleClassName );
    127        } catch( final ClassNotFoundException e ) {
    128            LOG.error( e.getMessage(), e );
    129            throw new WikiException( "Could not instantiate LoginModule class.", e );
    130        }
    131
    132        // Initialize the LoginModule options
    133        initLoginModuleOptions( props );
    134    }
    135
    136    /**
    137     * {@inheritDoc}
    138     */
    139    @Override
    140    public boolean isContainerAuthenticated() {
    141        try {
    142            final Authorizer authorizer = m_engine.getManager( AuthorizationManager.class ).getAuthorizer();
    143            if ( authorizer instanceof WebContainerAuthorizer ) {
    144                 return ( ( WebContainerAuthorizer )authorizer ).isContainerAuthorized();
    145            }
    146        } catch ( final WikiException e ) {
    147            // It's probably ok to fail silently...
    148        }
    149        return false;
    150    }
    151
    152    /**
    153     * {@inheritDoc}
    154     */
    155    @Override
    156    public boolean login( final HttpServletRequest request ) throws WikiSecurityException {
    157        final HttpSession httpSession = request.getSession();
    158        final Session session = SessionMonitor.getInstance( m_engine ).find( httpSession );
    159        final AuthenticationManager authenticationMgr = m_engine.getManager( AuthenticationManager.class );
    160        final AuthorizationManager authorizationMgr = m_engine.getManager( AuthorizationManager.class );
    161        CallbackHandler handler = null;
    162        final Map< String, String > options = EMPTY_MAP;
    163
    164        // If user not authenticated, check if container logged them in, or if there's an authentication cookie
    165        if ( !session.isAuthenticated() ) {
    166            // Create a callback handler
    167            handler = new WebContainerCallbackHandler( m_engine, request );
    168
    169            // Execute the container login module, then (if that fails) the cookie auth module
    170            Set< Principal > principals = authenticationMgr.doJAASLogin( WebContainerLoginModule.class, handler, options );
    171            if (principals.isEmpty() && authenticationMgr.allowsCookieAuthentication() ) {
    172                principals = authenticationMgr.doJAASLogin( CookieAuthenticationLoginModule.class, handler, options );
    173            }
    174
    175            // If the container logged the user in successfully, tell the Session (and add all the Principals)
    176            if (!principals.isEmpty()) {
    177                fireEvent( WikiSecurityEvent.LOGIN_AUTHENTICATED, getLoginPrincipal( principals ), session );
    178                for( final Principal principal : principals ) {
    179                    fireEvent( WikiSecurityEvent.PRINCIPAL_ADD, principal, session );
    180                }
    181
    182                // Add all appropriate Authorizer roles
    183                injectAuthorizerRoles( session, authorizationMgr.getAuthorizer(), request );
    184            }
    185        }
    186
    187        // If user still not authenticated, check if assertion cookie was supplied
    188        if ( !session.isAuthenticated() && authenticationMgr.allowsCookieAssertions() ) {
    189            // Execute the cookie assertion login module
    190            final Set< Principal > principals = authenticationMgr.doJAASLogin( CookieAssertionLoginModule.class, handler, options );
    191            if (!principals.isEmpty()) {
    192                fireEvent( WikiSecurityEvent.LOGIN_ASSERTED, getLoginPrincipal( principals ), session);
    193            }
    194        }
    195
    196        // If user still anonymous, use the remote address
    197        if( session.isAnonymous() ) {
    198            final Set< Principal > principals = authenticationMgr.doJAASLogin( AnonymousLoginModule.class, handler, options );
    199            if(!principals.isEmpty()) {
    200                fireEvent( WikiSecurityEvent.LOGIN_ANONYMOUS, getLoginPrincipal( principals ), session );
    201                return true;
    202            }
    203        }
    204
    205        // If by some unusual turn of events the Anonymous login module doesn't work, login failed!
    206        return false;
    207    }
    208
    209    /**
    210     * {@inheritDoc}
    211     */
    212    @Override
    213    public boolean login( final Session session, final HttpServletRequest request, final String username, final String password ) throws WikiSecurityException {
    214        if ( session == null ) {
    215            LOG.error( "No wiki session provided, cannot log in." );
    216            return false;
    217        }
    218
    219        // Protect against brute-force password guessing if configured to do so
    220        if ( m_throttleLogins ) {
    221            delayLogin( username );
    222        }
    223
    224        final CallbackHandler handler = new WikiCallbackHandler( m_engine, null, username, password );
    225
    226        // Execute the user's specified login module
    227        final Set< Principal > principals = doJAASLogin( m_loginModuleClass, handler, m_loginModuleOptions );
    228        if(!principals.isEmpty()) {
    229            fireEvent(WikiSecurityEvent.LOGIN_AUTHENTICATED, getLoginPrincipal( principals ), session );
    230            for ( final Principal principal : principals ) {
    231                fireEvent( WikiSecurityEvent.PRINCIPAL_ADD, principal, session );
    232            }
    233
    234            // Add all appropriate Authorizer roles
    235            injectAuthorizerRoles( session, m_engine.getManager( AuthorizationManager.class ).getAuthorizer(), null );
    236
    237            return true;
    238        }
    239        return false;
    240    }
    241
    242    /**
    243     *  This method builds a database of login names that are being attempted, and will try to delay if there are too many requests coming
    244     *  in for the same username.
    245     *  <p>
    246     *  The current algorithm uses 2^loginattempts as the delay in milliseconds, i.e. at 10 login attempts it'll add 1.024 seconds to the login.
    247     *
    248     *  @param username The username that is being logged in
    249     */
    250    private void delayLogin( final String username ) {
    251        try {
    252            m_lastLoginAttempts.cleanup( LASTLOGINS_CLEANUP_TIME );
    253            final int count = m_lastLoginAttempts.count( username );
    254
    255            final long delay = Math.min( 1L << count, MAX_LOGIN_DELAY );
    256            LOG.debug( "Sleeping for " + delay + " ms to allow login." );
    257            Thread.sleep( delay );
    258
    259            m_lastLoginAttempts.add( username );
    260        } catch( final InterruptedException e ) {
    261            // FALLTHROUGH is fine
    262        }
    263    }
    264
    265    /**
    266     * {@inheritDoc}
    267     */
    268    @Override
    269    public void logout( final HttpServletRequest request ) {
    270        if( request == null ) {
    271            LOG.error( "No HTTP reqest provided; cannot log out." );
    272            return;
    273        }
    274
    275        final HttpSession session = request.getSession();
    276        final String sid = ( session == null ) ? "(null)" : session.getId();
    277        LOG.debug( "Invalidating Session for session ID= {}", sid );
    278        // Retrieve the associated Session and clear the Principal set
    279        final Session wikiSession = Wiki.session().find( m_engine, request );
    280        final Principal originalPrincipal = wikiSession.getLoginPrincipal();
    281        wikiSession.invalidate();
    282
    283        // Remove the wikiSession from the WikiSession cache
    284        Wiki.session().remove( m_engine, request );
    285
    286        // We need to flush the HTTP session too
    287        if( session != null ) {
    288            session.invalidate();
    289        }
    290
    291        // Log the event
    292        fireEvent( WikiSecurityEvent.LOGOUT, originalPrincipal, null );
    293    }
    294
    295    /**
    296     * {@inheritDoc}
    297     */
    298    @Override
    299    public boolean allowsCookieAssertions() {
    300        return m_allowsCookieAssertions;
    301    }
    302
    303    /**
    304     * {@inheritDoc}
    305     */
    306    @Override
    307    public boolean allowsCookieAuthentication() {
    308        return m_allowsCookieAuthentication;
    309    }
    310
    311    /**
    312     * {@inheritDoc}
    313     */
    314    @Override
    315    public Set< Principal > doJAASLogin( final Class< ? extends LoginModule > clazz,
    316                                         final CallbackHandler handler,
    317                                         final Map< String, String > options ) throws WikiSecurityException {
    318        // Instantiate the login module
    319        final LoginModule loginModule;
    320        try {
    321            loginModule = ClassUtil.buildInstance( clazz );
    322        } catch( final ReflectiveOperationException e ) {
    323            throw new WikiSecurityException( e.getMessage(), e );
    324        }
    325
    326        // Initialize the LoginModule
    327        final Subject subject = new Subject();
    328        loginModule.initialize( subject, handler, EMPTY_MAP, options );
    329
    330        // Try to log in:
    331        boolean loginSucceeded = false;
    332        boolean commitSucceeded = false;
    333        try {
    334            loginSucceeded = loginModule.login();
    335            if( loginSucceeded ) {
    336                commitSucceeded = loginModule.commit();
    337            }
    338        } catch( final LoginException e ) {
    339            // Login or commit failed! No principal for you!
    340        }
    341
    342        // If we successfully logged in & committed, return all the principals
    343        if( loginSucceeded && commitSucceeded ) {
    344            return subject.getPrincipals();
    345        }
    346        return NO_PRINCIPALS;
    347    }
    348
    349    // events processing .......................................................
    350
    351    /**
    352     * {@inheritDoc}
    353     */
    354    @Override
    355    public synchronized void addWikiEventListener( final WikiEventListener listener ) {
    356        WikiEventManager.addWikiEventListener( this, listener );
    357    }
    358
    359    /**
    360     * {@inheritDoc}
    361     */
    362    @Override
    363    public synchronized void removeWikiEventListener( final WikiEventListener listener ) {
    364        WikiEventManager.removeWikiEventListener( this, listener );
    365    }
    366
    367    /**
    368     * Initializes the options Map supplied to the configured LoginModule every time it is invoked. The properties and values extracted from
    369     * <code>jspwiki.properties</code> are of the form <code>jspwiki.loginModule.options.<var>param</var> = <var>value</var>, where
    370     * <var>param</var> is the key name, and <var>value</var> is the value.
    371     *
    372     * @param props the properties used to initialize JSPWiki
    373     * @throws IllegalArgumentException if any of the keys are duplicated
    374     */
    375    private void initLoginModuleOptions( final Properties props ) {
    376        for( final Object key : props.keySet() ) {
    377            final String propName = key.toString();
    378            if( propName.startsWith( PREFIX_LOGIN_MODULE_OPTIONS ) ) {
    379                // Extract the option name and value
    380                final String optionKey = propName.substring( PREFIX_LOGIN_MODULE_OPTIONS.length() ).trim();
    381                if( !optionKey.isEmpty() ) {
    382                    final String optionValue = props.getProperty( propName );
    383
    384                    // Make sure the key is unique before stashing the key/value pair
    385                    if ( m_loginModuleOptions.containsKey( optionKey ) ) {
    386                        throw new IllegalArgumentException( "JAAS LoginModule key " + propName + " cannot be specified twice!" );
    387                    }
    388                    m_loginModuleOptions.put( optionKey, optionValue );
    389                }
    390            }
    391        }
    392    }
    393
    394    /**
    395     * After successful login, this method is called to inject authorized role Principals into the Session. To determine which roles
    396     * should be injected, the configured Authorizer is queried for the roles it knows about by calling  {@link Authorizer#getRoles()}.
    397     * Then, each role returned by the authorizer is tested by calling {@link Authorizer#isUserInRole(Session, Principal)}. If this
    398     * check fails, and the Authorizer is of type WebAuthorizer, the role is checked again by calling
    399     * {@link WebAuthorizer#isUserInRole(HttpServletRequest, Principal)}). Any roles that pass the test are injected into the Subject by
    400     * firing appropriate authentication events.
    401     *
    402     * @param session the user's current Session
    403     * @param authorizer the Engine's configured Authorizer
    404     * @param request the user's HTTP session, which may be <code>null</code>
    405     */
    406    private void injectAuthorizerRoles( final Session session, final Authorizer authorizer, final HttpServletRequest request ) {
    407        // Test each role the authorizer knows about
    408        for( final Principal role : authorizer.getRoles() ) {
    409            // Test the Authorizer
    410            if( authorizer.isUserInRole( session, role ) ) {
    411                fireEvent( WikiSecurityEvent.PRINCIPAL_ADD, role, session );
    412                LOG.debug( "Added authorizer role {}.", role.getName() );
    413            // If web authorizer, test the request.isInRole() method also
    414            } else if ( request != null && authorizer instanceof WebAuthorizer ) {
    415                final WebAuthorizer wa = ( WebAuthorizer )authorizer;
    416                if ( wa.isUserInRole( request, role ) ) {
    417                    fireEvent( WikiSecurityEvent.PRINCIPAL_ADD, role, session );
    418                    LOG.debug( "Added container role {}.",role.getName() );
    419                }
    420            }
    421        }
    422    }
    423
    424}