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}