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 }