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}