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