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}