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    }