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.TextUtil; 040import org.apache.wiki.util.TimedCounterList; 041 042import javax.security.auth.Subject; 043import javax.security.auth.callback.CallbackHandler; 044import javax.security.auth.login.LoginException; 045import javax.security.auth.spi.LoginModule; 046import javax.servlet.http.HttpServletRequest; 047import javax.servlet.http.HttpSession; 048import java.lang.reflect.InvocationTargetException; 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 = ( Class< ? extends LoginModule > )Class.forName( 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.size() == 0 && 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 of the Principals) 176 if ( principals.size() > 0 ) { 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.size() > 0 ) { 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.size() > 0 ) { 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.size() > 0 ) { 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 if( log.isDebugEnabled() ) { 278 log.debug( "Invalidating Session for session ID=" + sid ); 279 } 280 // Retrieve the associated Session and clear the Principal set 281 final Session wikiSession = Wiki.session().find( m_engine, request ); 282 final Principal originalPrincipal = wikiSession.getLoginPrincipal(); 283 wikiSession.invalidate(); 284 285 // Remove the wikiSession from the WikiSession cache 286 Wiki.session().remove( m_engine, request ); 287 288 // We need to flush the HTTP session too 289 if( session != null ) { 290 session.invalidate(); 291 } 292 293 // Log the event 294 fireEvent( WikiSecurityEvent.LOGOUT, originalPrincipal, null ); 295 } 296 297 /** 298 * {@inheritDoc} 299 */ 300 @Override 301 public boolean allowsCookieAssertions() { 302 return m_allowsCookieAssertions; 303 } 304 305 /** 306 * {@inheritDoc} 307 */ 308 @Override 309 public boolean allowsCookieAuthentication() { 310 return m_allowsCookieAuthentication; 311 } 312 313 /** 314 * {@inheritDoc} 315 */ 316 @Override 317 public Set< Principal > doJAASLogin( final Class< ? extends LoginModule > clazz, 318 final CallbackHandler handler, 319 final Map< String, String > options ) throws WikiSecurityException { 320 // Instantiate the login module 321 final LoginModule loginModule; 322 try { 323 loginModule = clazz.getDeclaredConstructor().newInstance(); 324 } catch( final InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e ) { 325 throw new WikiSecurityException( e.getMessage(), e ); 326 } 327 328 // Initialize the LoginModule 329 final Subject subject = new Subject(); 330 loginModule.initialize( subject, handler, EMPTY_MAP, options ); 331 332 // Try to log in: 333 boolean loginSucceeded = false; 334 boolean commitSucceeded = false; 335 try { 336 loginSucceeded = loginModule.login(); 337 if( loginSucceeded ) { 338 commitSucceeded = loginModule.commit(); 339 } 340 } catch( final LoginException e ) { 341 // Login or commit failed! No principal for you! 342 } 343 344 // If we successfully logged in & committed, return all the principals 345 if( loginSucceeded && commitSucceeded ) { 346 return subject.getPrincipals(); 347 } 348 return NO_PRINCIPALS; 349 } 350 351 // events processing ....................................................... 352 353 /** 354 * {@inheritDoc} 355 */ 356 @Override 357 public synchronized void addWikiEventListener( final WikiEventListener listener ) { 358 WikiEventManager.addWikiEventListener( this, listener ); 359 } 360 361 /** 362 * {@inheritDoc} 363 */ 364 @Override 365 public synchronized void removeWikiEventListener( final WikiEventListener listener ) { 366 WikiEventManager.removeWikiEventListener( this, listener ); 367 } 368 369 /** 370 * Initializes the options Map supplied to the configured LoginModule every time it is invoked. The properties and values extracted from 371 * <code>jspwiki.properties</code> are of the form <code>jspwiki.loginModule.options.<var>param</var> = <var>value</var>, where 372 * <var>param</var> is the key name, and <var>value</var> is the value. 373 * 374 * @param props the properties used to initialize JSPWiki 375 * @throws IllegalArgumentException if any of the keys are duplicated 376 */ 377 private void initLoginModuleOptions( final Properties props ) { 378 for( final Object key : props.keySet() ) { 379 final String propName = key.toString(); 380 if( propName.startsWith( PREFIX_LOGIN_MODULE_OPTIONS ) ) { 381 // Extract the option name and value 382 final String optionKey = propName.substring( PREFIX_LOGIN_MODULE_OPTIONS.length() ).trim(); 383 if( !optionKey.isEmpty() ) { 384 final String optionValue = props.getProperty( propName ); 385 386 // Make sure the key is unique before stashing the key/value pair 387 if ( m_loginModuleOptions.containsKey( optionKey ) ) { 388 throw new IllegalArgumentException( "JAAS LoginModule key " + propName + " cannot be specified twice!" ); 389 } 390 m_loginModuleOptions.put( optionKey, optionValue ); 391 } 392 } 393 } 394 } 395 396 /** 397 * After successful login, this method is called to inject authorized role Principals into the Session. To determine which roles 398 * should be injected, the configured Authorizer is queried for the roles it knows about by calling {@link Authorizer#getRoles()}. 399 * Then, each role returned by the authorizer is tested by calling {@link Authorizer#isUserInRole(Session, Principal)}. If this 400 * check fails, and the Authorizer is of type WebAuthorizer, the role is checked again by calling 401 * {@link WebAuthorizer#isUserInRole(HttpServletRequest, Principal)}). Any roles that pass the test are injected into the Subject by 402 * firing appropriate authentication events. 403 * 404 * @param session the user's current Session 405 * @param authorizer the Engine's configured Authorizer 406 * @param request the user's HTTP session, which may be <code>null</code> 407 */ 408 private void injectAuthorizerRoles( final Session session, final Authorizer authorizer, final HttpServletRequest request ) { 409 // Test each role the authorizer knows about 410 for( final Principal role : authorizer.getRoles() ) { 411 // Test the Authorizer 412 if( authorizer.isUserInRole( session, role ) ) { 413 fireEvent( WikiSecurityEvent.PRINCIPAL_ADD, role, session ); 414 if( log.isDebugEnabled() ) { 415 log.debug( "Added authorizer role " + role.getName() + "." ); 416 } 417 // If web authorizer, test the request.isInRole() method also 418 } else if ( request != null && authorizer instanceof WebAuthorizer ) { 419 final WebAuthorizer wa = ( WebAuthorizer )authorizer; 420 if ( wa.isUserInRole( request, role ) ) { 421 fireEvent( WikiSecurityEvent.PRINCIPAL_ADD, role, session ); 422 if ( log.isDebugEnabled() ) { 423 log.debug( "Added container role " + role.getName() + "." ); 424 } 425 } 426 } 427 } 428 } 429 430}