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