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.messaging.service.security;
023
024import java.io.UnsupportedEncodingException;
025import java.util.Date;
026
027import javax.servlet.http.HttpSession;
028
029import org.granite.clustering.DistributedData;
030import org.granite.context.GraniteContext;
031import org.granite.logging.Logger;
032import org.granite.messaging.amf.process.AMF3MessageProcessor;
033import org.granite.messaging.webapp.HttpGraniteContext;
034import org.granite.messaging.webapp.ServletGraniteContext;
035import org.granite.util.Base64;
036
037import 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 */
045public 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}