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 }