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.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 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 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}