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