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.messaging.service.security;
022    
023    import java.io.UnsupportedEncodingException;
024    import java.util.Date;
025    
026    import javax.servlet.http.HttpSession;
027    
028    import org.granite.clustering.DistributedData;
029    import org.granite.context.GraniteContext;
030    import org.granite.logging.Logger;
031    import org.granite.messaging.amf.process.AMF3MessageProcessor;
032    import org.granite.messaging.webapp.HttpGraniteContext;
033    import org.granite.messaging.webapp.ServletGraniteContext;
034    import org.granite.util.Base64;
035    
036    import flex.messaging.messages.Message;
037    
038    /**
039     * Abstract implementation of the {@link SecurityService} interface. This class mainly contains
040     * utility methods helping with actual implementations.
041     * 
042     * @author Franck WOLFF
043     */
044    public abstract class AbstractSecurityService implements SecurityService {
045    
046        private static final Logger log = Logger.getLogger(AbstractSecurityService.class);
047    
048        public static final String AUTH_TYPE = "granite-security";
049    
050        /**
051         * A default implementation of the basic login method, passing null as the extra charset
052         * parameter. Mainly here for compatibility purpose.
053         * 
054         * @param credentials the login:password pair (must be a base64/ISO-8859-1 encoded string).
055         */
056        public void login(Object credentials) throws SecurityServiceException {
057            login(credentials, null);
058            }
059    
060            /**
061         * Try to login by using remote credentials (see Flex method RemoteObject.setRemoteCredentials()).
062         * This method must be called at the beginning of {@link SecurityService#authorize(AbstractSecurityContext)}.
063         * 
064         * @param context the current security context.
065         * @throws SecurityServiceException if login fails.
066         */
067        protected void startAuthorization(AbstractSecurityContext context) throws SecurityServiceException {
068            // Get credentials set with RemoteObject.setRemoteCredentials() and login.
069            Object credentials = context.getMessage().getHeader(Message.REMOTE_CREDENTIALS_HEADER);
070            if (credentials != null && !("".equals(credentials)))
071                login(credentials, (String)context.getMessage().getHeader(Message.REMOTE_CREDENTIALS_CHARSET_HEADER));
072            
073            // Check session expiration
074            if (GraniteContext.getCurrentInstance() instanceof ServletGraniteContext) {
075                    HttpSession session = ((ServletGraniteContext)GraniteContext.getCurrentInstance()).getSession(false);
076                    if (session == null)
077                            return;
078                    
079                    long serverTime = new Date().getTime();
080                    Long lastAccessedTime = (Long)session.getAttribute(GraniteContext.SESSION_LAST_ACCESSED_TIME_KEY);
081                    if (lastAccessedTime != null && lastAccessedTime + session.getMaxInactiveInterval()*1000L + 1000L < serverTime) {
082                            log.info("No user-initiated action since last access, force session invalidation");
083                            session.invalidate();
084                    }
085            }
086        }
087    
088        /**
089         * Invoke a service method (EJB3, Spring, Seam, etc...) after a successful authorization.
090         * This method must be called at the end of {@link SecurityService#authorize(AbstractSecurityContext)}.
091         * 
092         * @param context the current security context.
093         * @throws Exception if anything goes wrong with service invocation.
094         */
095        protected Object endAuthorization(AbstractSecurityContext context) throws Exception {
096            return context.invoke();
097        }
098        
099        /**
100         * A security service can optionally indicate that it's able to authorize requests that are not HTTP requests
101         * (websockets). In this case the method {@link SecurityService#authorize(AbstractSecurityContext)} will be 
102         * invoked in a {@link ServletGraniteContext} and not in a {@link HttpGraniteContext}
103         * @return true is a {@link HttpGraniteContext} is mandated
104         */
105        public boolean acceptsContext() {
106            return GraniteContext.getCurrentInstance() instanceof HttpGraniteContext;
107        }
108    
109        /**
110         * Decode credentails encoded in base 64 (in the form of "username:password"), as they have been
111         * sent by a RemoteObject.
112         * 
113         * @param credentials base 64 encoded credentials.
114         * @return an array containing two decoded Strings, username and password.
115         * @throws IllegalArgumentException if credentials isn't a String.
116         * @throws SecurityServiceException if credentials are invalid (bad encoding or missing ':').
117         */
118        protected String[] decodeBase64Credentials(Object credentials, String charset) {
119            if (!(credentials instanceof String))
120                throw new IllegalArgumentException("Credentials should be a non null String: " +
121                    (credentials != null ? credentials.getClass().getName() : null));
122    
123            if (charset == null)
124                    charset = "ISO-8859-1";
125            
126            byte[] bytes = Base64.decode((String)credentials);
127            String decoded;
128            try {
129                    decoded = new String(bytes, charset);
130            }
131            catch (UnsupportedEncodingException e) {
132                throw SecurityServiceException.newInvalidCredentialsException("ISO-8859-1 encoding not supported ???");
133            }
134    
135            int colon = decoded.indexOf(':');
136            if (colon == -1)
137                throw SecurityServiceException.newInvalidCredentialsException("No colon");
138    
139            return new String[] {decoded.substring(0, colon), decoded.substring(colon + 1)};
140        }
141        
142        /**
143         * Handle a security exception. This method is called in
144         * {@link AMF3MessageProcessor#processCommandMessage(flex.messaging.messages.CommandMessage)}
145         * whenever a SecurityService occurs and does nothing by default.
146         * 
147         * @param e the security exception.
148         */
149            public void handleSecurityException(SecurityServiceException e) {
150        }
151    
152        /**
153         * Try to save current credentials in distributed data, typically a user session attribute. This method
154         * must be called at the end of a successful {@link SecurityService#login(Object)} operation and is useful
155         * in clustered environments with session replication in order to transparently re-authenticate the
156         * user when failing over.
157         * 
158         * @param credentials the credentials to be saved in distributed data.
159         */
160            protected void endLogin(Object credentials, String charset) {
161                    try {
162                            DistributedData gdd = GraniteContext.getCurrentInstance().getGraniteConfig().getDistributedDataFactory().getInstance();
163                            if (gdd != null) {
164                                    gdd.setCredentials(credentials);
165                                    gdd.setCredentialsCharset(charset);
166                            }
167                    }
168                    catch (Exception e) {
169                            log.error(e, "Could not save credentials in distributed data");
170                    }
171        }
172        
173            /**
174             * Try to re-authenticate the current user with credentials previously saved in distributed data.
175             * This method must be called in the {@link SecurityService#authorize(AbstractSecurityContext)}
176             * method when the current user principal is null.
177             * 
178             * @return <tt>true</tt> if relogin was successful, <tt>false</tt> otherwise.
179             * 
180             * @see #endLogin(Object, String)
181             */
182        protected boolean tryRelogin() {
183            try {
184                            DistributedData gdd = GraniteContext.getCurrentInstance().getGraniteConfig().getDistributedDataFactory().getInstance();
185                            if (gdd != null) {
186                                    Object credentials = gdd.getCredentials();
187                            if (credentials != null) {
188                                    String charset = gdd.getCredentialsCharset();
189                                    try {
190                                            login(credentials, charset);
191                                            return true;
192                                    }
193                                    catch (SecurityServiceException e) {
194                                    }
195                            }
196                            }
197            }
198            catch (Exception e) {
199                    log.error(e, "Could not relogin with credentials found in distributed data");
200            }
201            return false;
202        }
203    
204        /**
205         * Try to remove credentials previously saved in distributed data. This method must be called in the
206         * {@link SecurityService#logout()} method.
207         * 
208             * @see #endLogin(Object, String)
209         */
210            protected void endLogout() {
211                    try {
212                            DistributedData gdd = GraniteContext.getCurrentInstance().getGraniteConfig().getDistributedDataFactory().getInstance();
213                            if (gdd != null) {
214                                    gdd.removeCredentials();
215                                    gdd.removeCredentialsCharset();
216                            }
217                    }
218                    catch (Exception e) {
219                            log.error(e, "Could not remove credentials from distributed data");
220                    }
221        }
222    }