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 */
022 package org.granite.spring.security;
023
024 import java.lang.reflect.InvocationTargetException;
025 import java.lang.reflect.Method;
026 import java.util.Arrays;
027 import java.util.List;
028 import java.util.Map;
029
030 import javax.servlet.http.HttpServletRequest;
031 import javax.servlet.http.HttpServletResponse;
032 import javax.servlet.http.HttpSession;
033
034 import org.granite.context.GraniteContext;
035 import org.granite.logging.Logger;
036 import org.granite.messaging.service.security.AbstractSecurityContext;
037 import org.granite.messaging.service.security.AbstractSecurityService;
038 import org.granite.messaging.service.security.SecurityServiceException;
039 import org.granite.messaging.webapp.HttpGraniteContext;
040 import org.granite.messaging.webapp.ServletGraniteContext;
041 import org.springframework.beans.factory.BeanFactoryUtils;
042 import org.springframework.context.ApplicationContext;
043 import org.springframework.context.ApplicationContextAware;
044 import org.springframework.context.ApplicationEventPublisher;
045 import org.springframework.context.ApplicationEventPublisherAware;
046 import org.springframework.security.access.AccessDeniedException;
047 import org.springframework.security.authentication.AnonymousAuthenticationToken;
048 import org.springframework.security.authentication.AuthenticationManager;
049 import org.springframework.security.authentication.AuthenticationTrustResolver;
050 import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
051 import org.springframework.security.authentication.BadCredentialsException;
052 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
053 import org.springframework.security.authentication.encoding.PasswordEncoder;
054 import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
055 import org.springframework.security.core.Authentication;
056 import org.springframework.security.core.AuthenticationException;
057 import org.springframework.security.core.GrantedAuthority;
058 import org.springframework.security.core.context.SecurityContext;
059 import org.springframework.security.core.context.SecurityContextHolder;
060 import org.springframework.security.core.userdetails.UsernameNotFoundException;
061 import org.springframework.security.web.authentication.session.SessionAuthenticationException;
062 import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
063 import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
064 import org.springframework.security.web.context.HttpRequestResponseHolder;
065 import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
066 import org.springframework.security.web.context.SecurityContextRepository;
067 import org.springframework.web.context.support.WebApplicationContextUtils;
068
069
070 /**
071 * @author Bouiaw
072 * @author wdrai
073 */
074 @SuppressWarnings("deprecation")
075 public 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 }