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