001/** 002 * GRANITE DATA SERVICES 003 * Copyright (C) 2006-2013 GRANITE DATA SERVICES S.A.S. 004 * 005 * This file is part of the Granite Data Services Platform. 006 * 007 * Granite Data Services is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * Granite Data Services is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser 015 * General Public License for more details. 016 * 017 * You should have received a copy of the GNU Lesser General Public 018 * License along with this library; if not, write to the Free Software 019 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 020 * USA, or see <http://www.gnu.org/licenses/>. 021 */ 022package org.granite.spring.security; 023 024import java.lang.reflect.InvocationTargetException; 025import java.lang.reflect.Method; 026import java.util.Arrays; 027import java.util.List; 028import java.util.Map; 029 030import javax.servlet.http.HttpServletRequest; 031import javax.servlet.http.HttpServletResponse; 032import javax.servlet.http.HttpSession; 033 034import org.granite.context.GraniteContext; 035import org.granite.logging.Logger; 036import org.granite.messaging.service.security.AbstractSecurityContext; 037import org.granite.messaging.service.security.AbstractSecurityService; 038import org.granite.messaging.service.security.SecurityServiceException; 039import org.granite.messaging.webapp.HttpGraniteContext; 040import org.granite.messaging.webapp.ServletGraniteContext; 041import org.springframework.beans.factory.BeanFactoryUtils; 042import org.springframework.context.ApplicationContext; 043import org.springframework.context.ApplicationContextAware; 044import org.springframework.context.ApplicationEventPublisher; 045import org.springframework.context.ApplicationEventPublisherAware; 046import org.springframework.security.access.AccessDeniedException; 047import org.springframework.security.authentication.AnonymousAuthenticationToken; 048import org.springframework.security.authentication.AuthenticationManager; 049import org.springframework.security.authentication.AuthenticationTrustResolver; 050import org.springframework.security.authentication.AuthenticationTrustResolverImpl; 051import org.springframework.security.authentication.BadCredentialsException; 052import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 053import org.springframework.security.authentication.encoding.PasswordEncoder; 054import org.springframework.security.authentication.event.AuthenticationSuccessEvent; 055import org.springframework.security.core.Authentication; 056import org.springframework.security.core.AuthenticationException; 057import org.springframework.security.core.GrantedAuthority; 058import org.springframework.security.core.context.SecurityContext; 059import org.springframework.security.core.context.SecurityContextHolder; 060import org.springframework.security.core.userdetails.UsernameNotFoundException; 061import org.springframework.security.web.authentication.session.SessionAuthenticationException; 062import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; 063import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy; 064import org.springframework.security.web.context.HttpRequestResponseHolder; 065import org.springframework.security.web.context.HttpSessionSecurityContextRepository; 066import org.springframework.security.web.context.SecurityContextRepository; 067import org.springframework.web.context.support.WebApplicationContextUtils; 068 069 070/** 071 * @author Bouiaw 072 * @author wdrai 073 */ 074@SuppressWarnings("deprecation") 075public class SpringSecurity3Service extends AbstractSecurityService implements ApplicationContextAware, ApplicationEventPublisherAware { 076 077 private static final Logger log = Logger.getLogger(SpringSecurity3Service.class); 078 079 private static final String FILTER_APPLIED = "__spring_security_scpf_applied"; 080 private static final String SECURITY_SERVICE_APPLIED = "__spring_security_granite_service_applied"; 081 082 private ApplicationContext applicationContext = null; 083 private ApplicationEventPublisher eventPublisher = null; 084 private AuthenticationExtension authenticationExtension = new DefaultAuthenticationExtension(); 085 private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl(); 086 private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository(); 087 private AbstractSpringSecurity3Interceptor securityInterceptor = null; 088 private SessionAuthenticationStrategy sessionAuthenticationStrategy = new SessionFixationProtectionStrategy(); 089 private PasswordEncoder passwordEncoder = null; 090 private boolean allowAnonymousAccess = false; 091 private Method getRequest = null; 092 private Method getResponse = null; 093 094 095 public SpringSecurity3Service() { 096 log.debug("Starting Spring 3 Security Service"); 097 try { 098 getRequest = HttpRequestResponseHolder.class.getDeclaredMethod("getRequest"); 099 getRequest.setAccessible(true); 100 getResponse = HttpRequestResponseHolder.class.getDeclaredMethod("getResponse"); 101 getResponse.setAccessible(true); 102 } 103 catch (Exception e) { 104 throw new RuntimeException("Could not get methods from HttpRequestResponseHolder", e); 105 } 106 } 107 108 public void setApplicationContext(ApplicationContext applicationContext) { 109 this.applicationContext = applicationContext; 110 } 111 112 public void setAuthenticationExtension(AuthenticationExtension authenticationExtension) { 113 if (authenticationExtension == null) 114 throw new NullPointerException("AuthenticationBuilder cannot be null"); 115 this.authenticationExtension = authenticationExtension; 116 } 117 118 public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { 119 this.eventPublisher = eventPublisher; 120 } 121 122 public void setAuthenticationManager(AuthenticationManager authenticationManager) { 123 authenticationExtension.setAuthenticationManager(authenticationManager); 124 } 125 126 public void setAuthenticationTrustResolver(AuthenticationTrustResolver authenticationTrustResolver) { 127 this.authenticationTrustResolver = authenticationTrustResolver; 128 } 129 130 public void setAllowAnonymousAccess(boolean allowAnonymousAccess) { 131 this.allowAnonymousAccess = allowAnonymousAccess; 132 } 133 134 public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) { 135 this.securityContextRepository = securityContextRepository; 136 } 137 138 public void setSecurityInterceptor(AbstractSpringSecurity3Interceptor securityInterceptor) { 139 this.securityInterceptor = securityInterceptor; 140 } 141 142 public void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionAuthenticationStrategy) { 143 if (sessionAuthenticationStrategy == null) 144 throw new NullPointerException("SessionAuthenticationStrategy cannot be null"); 145 this.sessionAuthenticationStrategy = sessionAuthenticationStrategy; 146 } 147 148 public void setPasswordEncoder(PasswordEncoder passwordEncoder) { 149 this.passwordEncoder = passwordEncoder; 150 } 151 152 public void configure(Map<String, String> params) { 153 log.debug("Configuring with parameters %s: ", params); 154 155 if (params.containsKey("authentication-manager-bean-name")) 156 authenticationExtension.setAuthenticationManagerBeanName(params.get("authentication-manager-bean-name")); 157 158 if (Boolean.TRUE.toString().equals(params.get("allow-anonymous-access"))) 159 allowAnonymousAccess = true; 160 } 161 162 public void login(Object credentials, String charset) { 163 List<String> decodedCredentials = Arrays.asList(decodeBase64Credentials(credentials, charset)); 164 165 if (!(GraniteContext.getCurrentInstance() instanceof HttpGraniteContext)) { 166 log.info("Login from non HTTP granite context ignored"); 167 return; 168 } 169 170 HttpGraniteContext graniteContext = (HttpGraniteContext)GraniteContext.getCurrentInstance(); 171 HttpServletRequest httpRequest = graniteContext.getRequest(); 172 boolean springFilterNotApplied = graniteContext.getRequest().getAttribute(FILTER_APPLIED) == null; 173 174 ApplicationContext appContext = applicationContext != null ? applicationContext 175 : WebApplicationContextUtils.getWebApplicationContext(graniteContext.getServletContext()); 176 if (appContext == null) 177 throw new IllegalStateException("No application context defined for Spring security service"); 178 authenticationExtension.setApplicationContext(appContext); 179 180 String user = decodedCredentials.get(0); 181 String password = decodedCredentials.get(1); 182 if (passwordEncoder != null) 183 password = passwordEncoder.encodePassword(password, null); 184 185 Authentication auth = authenticationExtension.buildAuthentication(user, password); 186 187 AuthenticationManager authenticationManager = authenticationExtension.selectAuthenticationManager(auth); 188 189 try { 190 Authentication authentication = authenticationManager.authenticate(auth); 191 192 if (authentication != null && !authenticationTrustResolver.isAnonymous(authentication)) { 193 try { 194 sessionAuthenticationStrategy.onAuthentication(authentication, httpRequest, graniteContext.getResponse()); 195 } 196 catch (SessionAuthenticationException e) { 197 log.debug(e, "SessionAuthenticationStrategy rejected the authentication object"); 198 SecurityContextHolder.clearContext(); 199 handleAuthenticationExceptions(e); 200 return; 201 } 202 } 203 204 log.debug("Define authentication and save to repo: %s", authentication != null ? authentication.getName() : "none"); 205 HttpRequestResponseHolder holder = new HttpRequestResponseHolder(graniteContext.getRequest(), graniteContext.getResponse()); 206 SecurityContext securityContext = securityContextRepository.loadContext(holder); 207 securityContext.setAuthentication(authentication); 208 SecurityContextHolder.setContext(securityContext); 209 try { 210 securityContextRepository.saveContext(securityContext, (HttpServletRequest)getRequest.invoke(holder), (HttpServletResponse)getResponse.invoke(holder)); 211 } 212 catch (Exception e) { 213 log.error(e, "Could not save context after authentication"); 214 } 215 216 if (eventPublisher != null) 217 eventPublisher.publishEvent(new AuthenticationSuccessEvent(authentication)); 218 219 endLogin(credentials, charset); 220 } 221 catch (AuthenticationException e) { 222 handleAuthenticationExceptions(e); 223 } 224 finally { 225 // Should not cleanup when authentication managed by the Spring filter 226 if (springFilterNotApplied) { 227 log.debug("Clear authentication after login"); 228 SecurityContextHolder.clearContext(); 229 } 230 } 231 232 log.debug("User %s logged in", user); 233 } 234 235 protected void handleAuthenticationExceptions(AuthenticationException e) { 236 if (e instanceof BadCredentialsException || e instanceof UsernameNotFoundException) 237 throw SecurityServiceException.newInvalidCredentialsException(e.getMessage()); 238 239 throw SecurityServiceException.newAuthenticationFailedException(e.getMessage()); 240 } 241 242 243 public Object authorize(AbstractSecurityContext context) throws Exception { 244 log.debug("Authorize %s on destination %s (secured: %b)", context, context.getDestination().getId(), context.getDestination().isSecured()); 245 246 startAuthorization(context); 247 248 ServletGraniteContext graniteContext = (ServletGraniteContext)GraniteContext.getCurrentInstance(); 249 250 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 251 HttpRequestResponseHolder holder = null; 252 253 boolean springFilterNotApplied = graniteContext.getRequest().getAttribute(FILTER_APPLIED) == null; 254 boolean reentrant = graniteContext.getRequest().getAttribute(SECURITY_SERVICE_APPLIED) != null; 255 // Manage security context here only if the Spring security filter has not already been applied and if we are not reentrant 256 try { 257 if (springFilterNotApplied && !reentrant) { 258 holder = new HttpRequestResponseHolder(graniteContext.getRequest(), graniteContext.getResponse()); 259 SecurityContext contextBeforeChainExecution = securityContextRepository.loadContext(holder); 260 SecurityContextHolder.setContext(contextBeforeChainExecution); 261 graniteContext.getRequest().setAttribute(SECURITY_SERVICE_APPLIED, true); 262 263 if (isAuthenticated(authentication)) { 264 log.debug("Thread was already authenticated: %s", authentication.getName()); 265 contextBeforeChainExecution.setAuthentication(authentication); 266 } 267 else { 268 authentication = contextBeforeChainExecution.getAuthentication(); 269 log.debug("Restore authentication from repository: %s", authentication != null ? authentication.getName() : "none"); 270 } 271 } 272 273 if (context.getDestination().isSecured()) { 274 if (!isAuthenticated(authentication) || (!allowAnonymousAccess && authentication instanceof AnonymousAuthenticationToken)) { 275 log.debug("User not authenticated!"); 276 throw SecurityServiceException.newNotLoggedInException("User not logged in"); 277 } 278 if (!userCanAccessService(context, authentication)) { 279 log.debug("Access denied for user %s", authentication != null ? authentication.getName() : "not authenticated"); 280 throw SecurityServiceException.newAccessDeniedException("User not in required role"); 281 } 282 } 283 284 Object returnedObject = securityInterceptor != null 285 ? securityInterceptor.invoke(context) 286 : endAuthorization(context); 287 288 return returnedObject; 289 } 290 catch (SecurityServiceException e) { 291 throw e; 292 } 293 catch (AccessDeniedException e) { 294 throw SecurityServiceException.newAccessDeniedException(e.getMessage()); 295 } 296 catch (InvocationTargetException e) { 297 handleAuthorizationExceptions(e); 298 throw e; 299 } 300 finally { 301 if (springFilterNotApplied && !reentrant) { 302 SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext(); 303 log.debug("Clear authentication and save to repo: %s", contextAfterChainExecution.getAuthentication() != null ? contextAfterChainExecution.getAuthentication().getName() : "none"); 304 SecurityContextHolder.clearContext(); 305 try { 306 securityContextRepository.saveContext(contextAfterChainExecution, (HttpServletRequest)getRequest.invoke(holder), (HttpServletResponse)getResponse.invoke(holder)); 307 } 308 catch (Exception e) { 309 log.error(e, "Could not extract wrapped context from holder"); 310 } 311 graniteContext.getRequest().removeAttribute(SECURITY_SERVICE_APPLIED); 312 } 313 } 314 } 315 316 @Override 317 public boolean acceptsContext() { 318 return GraniteContext.getCurrentInstance() instanceof ServletGraniteContext; 319 } 320 321 public void logout() { 322 ServletGraniteContext graniteContext = (ServletGraniteContext)GraniteContext.getCurrentInstance(); 323 boolean springFilterNotApplied = graniteContext.getRequest().getAttribute(FILTER_APPLIED) == null; 324 HttpSession session = graniteContext.getSession(false); 325 326 if (session != null && securityContextRepository.containsContext(graniteContext.getRequest())) { 327 authenticationExtension.endSession(session); 328 session.invalidate(); 329 } 330 331 if (springFilterNotApplied) 332 SecurityContextHolder.clearContext(); 333 } 334 335 protected boolean isUserInRole(Authentication authentication, String role) { 336 for (GrantedAuthority ga : authentication.getAuthorities()) { 337 if (ga.getAuthority().matches(role)) 338 return true; 339 } 340 return false; 341 } 342 343 protected boolean isAuthenticated(Authentication authentication) { 344 return authentication != null && authentication.isAuthenticated(); 345 } 346 347 protected boolean userCanAccessService(AbstractSecurityContext context, Authentication authentication) { 348 log.debug("Is authenticated as: %s", authentication.getName()); 349 350 for (String role : context.getDestination().getRoles()) { 351 if (isUserInRole(authentication, role)) { 352 log.debug("Allowed access to %s in role %s", authentication.getName(), role); 353 return true; 354 } 355 log.debug("Access denied for %s not in role %s", authentication.getName(), role); 356 } 357 return false; 358 } 359 360 protected void handleAuthorizationExceptions(InvocationTargetException e) { 361 for (Throwable t = e; t != null; t = t.getCause()) { 362 // Don't create a dependency to javax.ejb in SecurityService... 363 if (t instanceof SecurityException || 364 t instanceof AccessDeniedException || 365 "javax.ejb.EJBAccessException".equals(t.getClass().getName())) { 366 throw SecurityServiceException.newAccessDeniedException(t.getMessage()); 367 } 368 else if (t instanceof AuthenticationException) { 369 throw SecurityServiceException.newNotLoggedInException(t.getMessage()); 370 } 371 } 372 } 373 374 375 public static class DefaultAuthenticationExtension implements AuthenticationExtension { 376 377 private ApplicationContext applicationContext = null; 378 private AuthenticationManager authenticationManager = null; 379 private String authenticationManagerBeanName = null; 380 381 public void setApplicationContext(ApplicationContext applicationContext) { 382 this.applicationContext = applicationContext; 383 } 384 385 public void setAuthenticationManager(AuthenticationManager authenticationManager) { 386 this.authenticationManager = authenticationManager; 387 } 388 389 public void setAuthenticationManagerBeanName(String authenticationManagerBeanName) { 390 this.authenticationManagerBeanName = authenticationManagerBeanName; 391 } 392 393 @Override 394 public Authentication buildAuthentication(String user, String password) { 395 return new UsernamePasswordAuthenticationToken(user, password); 396 } 397 398 @Override 399 public AuthenticationManager selectAuthenticationManager(Authentication authentication) { 400 if (this.authenticationManager != null) 401 return this.authenticationManager; 402 403 Map<String, AuthenticationManager> authManagers = BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, AuthenticationManager.class); 404 405 if (authenticationManagerBeanName != null) { 406 AuthenticationManager authenticationManager = authManagers.get(authenticationManagerBeanName); 407 if (authenticationManager == null) { 408 log.error("AuthenticationManager bean not found " + authenticationManagerBeanName); 409 throw SecurityServiceException.newAuthenticationFailedException("Authentication failed"); 410 } 411 return authenticationManager; 412 } 413 else if (authManagers.size() > 1) { 414 log.error("More than one AuthenticationManager beans found, specify which one to use in Spring config <graniteds:security-service authentication-manager='myAuthManager'/> or in granite-config.xml <security type='org.granite.spring.security.SpringSecurity3Service'><param name='authentication-manager-bean-name' value='myAuthManager'/></security>"); 415 throw SecurityServiceException.newAuthenticationFailedException("Authentication failed"); 416 } 417 418 return authManagers.values().iterator().next(); 419 } 420 421 @Override 422 public void endSession(HttpSession session) { 423 } 424 } 425}