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