001package votorola.a.web.wic.authen; // Copyright 2008-2013, Michael Allan. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE. 002 003import java.io.*; 004import java.math.*; 005import java.security.*; 006import java.sql.SQLException; 007import java.util.logging.*; 008import javax.mail.internet.*; 009import javax.servlet.http.*; 010import org.apache.wicket.Session; 011import org.apache.wicket.ISessionListener; 012import org.apache.wicket.request.Request; 013import org.apache.wicket.request.http.WebResponse; 014import org.apache.wicket.request.mapper.parameter.PageParameters; 015import org.openid4java.consumer.*; 016import votorola.a.voter.*; 017import votorola.a.web.wic.*; 018import votorola.g.io.*; 019import votorola.g.lang.*; 020import votorola.g.logging.*; 021import votorola.g.web.CookieX; 022import votorola.g.web.wic.*; 023 024 025/** An authenticator based on OpenID and email handshakes. 026 */ 027public @ThreadSafe final class OpenIDAuthenticator extends Authenticator implements ISessionListener 028{ 029 030 031 /** Creates an OpenIDAuthenticator. 032 */ 033 public OpenIDAuthenticator( VOWicket _app ) 034 { 035 app = _app; 036 persistenceFile = new File( app.vsRun().voteServer().outDirectory(), 037 "login-persistence.serial" ); 038 if( persistenceFile.isFile() ) 039 { 040 try{ persistence = (Persistence)FileX.readObject( persistenceFile ); } 041 catch( ClassNotFoundException|IOException x ) 042 { 043 logger.config( "Unable to recreate persistence from previous run. Creating anew. Old persisted logins will be lost. Reason: " + x.toString() ); 044 if( !persistenceFile.delete() ) recreatePersistence(); 045 } 046 } 047 app.getSessionListeners().add( OpenIDAuthenticator.this ); // no need to unregister, registry does not outlive this listener 048 } 049 050 051 052 // ------------------------------------------------------------------------------------ 053 054 055 /** The name of the cookie that stores the login key of the user if login is 056 * cookie-persisted for that user. 057 * 058 * @see #newPersistKey(String,String) 059 */ 060 static final String COOKIE_PERSIST_KEY = "vo_loginPersistKey"; 061 // "voLogin.persistKey" would have been more standard 062 063 064 065 /** The name of the cookie that stores the email address of the user if login is 066 * cookie-persisted for that user. 067 */ 068 static final String COOKIE_PERSIST_USER_EMAIL = "vo_loginPersistUserEmail"; 069 // "voLogin.persistUserEmail" would have been more standard 070 071 072 073 /** The OpenID consumer manager. 074 */ 075 @ThreadRestricted("holds OpenIDAuthenticator.this") ConsumerManager consumerManager() 076 { 077 assert Thread.holdsLock( OpenIDAuthenticator.this ); // actually it's the object that is restricted, not this method 078 return consumerManager; 079 } 080 081 private ConsumerManager consumerManager = new ConsumerManager(); 082 083 084 085 /** Constructs a login persistence cookie. 086 */ 087 static Cookie newPersistCookie( final String name, final String value, final boolean toEncode, 088 final Request req ) 089 { 090 final Cookie cookie = new CookieX( name, value, toEncode ); 091 cookie.setMaxAge( COOKIE_PERSIST_DURATION_S ); 092 cookie.setPath( req.getContextPath() ); // allow access from pages other than WP_OpenIDLogin 093 return cookie; 094 } 095 096 097 098 /** Calculates a secure hash of the user's login fingerprint and returns it as a 099 * string in radix 36. 100 * 101 * @param ipAddress the IP address of the client or proxy. 102 */ 103 String newPersistKey( final String userEmail, final String ipAddress ) 104 { 105 if( !persistenceFile.isFile() ) recreatePersistence(); 106 final String fingerprint = persistence.hashSalt1() + "/" + userEmail 107 + "/" + ipAddress + "/" + persistence.hashSalt2(); 108 try 109 { 110 final MessageDigest digester = MessageDigest.getInstance( "SHA-256" ); 111 final byte[] digest = digester.digest( fingerprint.getBytes() ); 112 final BigInteger digestNumber = new BigInteger( /*positive*/1, digest ); 113 return digestNumber.toString( /*radix*/36 ).toUpperCase(); 114 } 115 catch( NoSuchAlgorithmException x ) { throw new RuntimeException( x ); } 116 } 117 118 119 120 /** The secure random number generator. 121 */ 122 @ThreadRestricted("holds OpenIDAuthenticator.this") SecureRandom secureRandomizer() 123 { 124 assert Thread.holdsLock( OpenIDAuthenticator.this ); // actually it's the object that is restricted, not this method 125 return secureRandomizer; 126 } 127 128 private final SecureRandom secureRandomizer = new SecureRandom(); 129 130 131 132 // - A u t h e n t i c a t o r -------------------------------------------------------- 133 134 135 public Class<? extends VPageHTML> loginPageClass() { return WP_OpenIDLogin.class; } 136 137 138 139 public void logOut() 140 { 141 final VRequestCycle cycle = VRequestCycle.get(); 142 final VSession session = VSession.get(); 143 final VSession.User user = session.user(); 144 if( user != null ) WP_OpenIDLogin.clearPersistence( user.email(), cycle ); 145 session.clearUser( cycle ); 146 } 147 148 149 150 public VPageHTML newLoginPage( PageParameters _pP ) { return new WP_OpenIDLogin( _pP ); } 151 152 153 154 // - I - S e s s i o n - L i s t e n e r ---------------------------------------------- 155 156 157 public void onCreated( final Session s ) 158 { 159 final VRequestCycle cycle = VRequestCycle.get(); 160 final Request req = cycle.getRequest(); 161 IDPair persistedUser = persistedUser( (HttpServletRequest)req.getContainerRequest(), app, 162 req, (WebResponse)cycle.getResponse() ); 163 final boolean isPersistent; 164 if( persistedUser == null ) 165 { 166 if( PRELOGIN_EMAIL == null ) return; 167 168 persistedUser = IDPair.fromEmail( PRELOGIN_EMAIL ); 169 isPersistent = false; 170 } 171 else isPersistent = true; 172 final VSession session = (VSession)s; 173 try 174 { 175 session.setUser( persistedUser, isPersistent, 176 app.vsRun().trustserver().getTraceNode(/*list ref*/null,persistedUser), cycle ); 177 } 178 catch( IOException|SQLException x ) { throw new RuntimeException( x ); } 179 } 180 181 182 183//// P r i v a t e /////////////////////////////////////////////////////////////////////// 184 185 186 private final VOWicket app; 187 188 189 190 // private IDPair apparentlyPersistedUser( HttpServletRequest _reqHS ) 191 // { 192 // return persistedUser( _reqHS, null, null ); 193 // } 194 195 196 197 /** The maximum duration of a cookie-persisted login for an inactive user, in seconds. 198 * Cookies are refreshed whenever the user is active during this time, thus 199 * effectively resetting the clock. 200 */ 201 private static final int COOKIE_PERSIST_DURATION_S = 86400/*s per day*/ * 14/*day*/; 202 203 204 205 private static final Logger logger = LoggerX.i( OpenIDAuthenticator.class ); 206 207 208 209 /** Extracts and verifies the identifier of the persistently authenticated user. 210 * Returns either the verified identifier, or null if well-formed persistence data 211 * cannot be retrieved from the request cookies, or if verification fails. 212 * 213 * @param authenAppOrNull null to skip authentication and refresh of cookies. 214 * @param reqOrNull may be null only if authenAppOrNull is null. 215 * @param resWOrNull the web response in which the persistence data is to be 216 * refreshed, if necessary. May be null only if authenAppOrNull is null. 217 */ 218 private static IDPair persistedUser( final HttpServletRequest reqHS, 219 final VOWicket authenAppOrNull, final Request reqOrNull, final WebResponse resWOrNull ) 220 { 221 // http://stackoverflow.com/questions/1354999/keep-me-logged-in-the-best-approach 222 223 assert !(reqOrNull == null || resWOrNull == null) || authenAppOrNull == null; 224 final Cookie[] cookies = reqHS.getCookies(); 225 if( cookies == null ) return null; 226 227 String userEmail = null; 228 String key = null; 229 for( final Cookie cookie: cookies ) 230 { 231 final String c = cookie.getName(); 232 if( c.equals( COOKIE_PERSIST_USER_EMAIL )) 233 { 234 userEmail = CookieX.decodedValue( cookie.getValue() ); 235 if( key != null ) break; // that's both of them 236 } 237 else if( c.equals( COOKIE_PERSIST_KEY )) 238 { 239 key = cookie.getValue(); 240 if( userEmail != null ) break; // that's both of them 241 } 242 } 243 if( key == null || userEmail == null ) return null; 244 245 try{ new InternetAddress( userEmail, /*strict*/true ); } 246 catch( AddressException x ) 247 { 248 logger.log( LoggerX.INFO, "ignoring invalid email address in cookie", x ); 249 return null; 250 } 251 252 final IDPair user = IDPair.fromEmail( userEmail ); 253 if( authenAppOrNull != null ) 254 { 255 final OpenIDAuthenticator auth = (OpenIDAuthenticator)authenAppOrNull.authenticator(); 256 final String expectedKey = auth.newPersistKey( userEmail, reqHS.getRemoteAddr() ); 257 if( !expectedKey.equals( key )) return null; 258 // user IP changed, or persistence salts file recreated, or phoney key 259 260 try 261 { 262 final UserSettings settings = new UserSettings( userEmail, 263 authenAppOrNull.vsRun().userTable() ); 264 if( !expectedKey.equals( settings.getLoginPersistKey() )) return null; 265 // user never originally authenticated, or settings table cleared 266 } 267 catch( SQLException x ) 268 { 269 logger.log( LoggerX.WARNING, "unable to authenticate persistent login", x ); 270 return null; 271 } 272 273 // refresh cookies 274 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 275 WebResponseX.addCookie( resWOrNull, newPersistCookie( COOKIE_PERSIST_USER_EMAIL, 276 userEmail, /*toEncode*/true, reqOrNull )); 277 WebResponseX.addCookie( resWOrNull, newPersistCookie( COOKIE_PERSIST_KEY, expectedKey, 278 /*toEncode*/false, reqOrNull )); 279 } 280 281 return user; 282 } 283 284 285 286 private volatile Persistence persistence; 287 288 289 290 private final File persistenceFile; 291 292 293 294 private @Warning("init call") void recreatePersistence() 295 { 296 persistence = new Persistence( OpenIDAuthenticator.this ); 297 try 298 { 299 FileX.writeObject( persistence, persistenceFile ); 300 } 301 catch( IOException x ) { logger.warning( "unable to store new persistence to file: " + x.toString() ); } 302 persistenceFile.setReadable( false, /*ownerOnly*/false ); // nobody can read/write 303 persistenceFile.setWritable( false, /*ownerOnly*/false ); 304 persistenceFile.setReadable( true, /*ownerOnly*/true ); // only owner can read/write 305 persistenceFile.setWritable( true, /*ownerOnly*/true ); 306 } 307 308 309 310 // ==================================================================================== 311 312 313 /** Parameters required for login persistence. 314 */ 315 private static final class Persistence implements Serializable 316 { 317 318 private static final long serialVersionUID = 0L; 319 320 321 Persistence( final OpenIDAuthenticator auth ) 322 { 323 final byte[] randomByteArray = new byte[2]; // raw format 324 final BigInteger randomN1, randomN2; 325 synchronized( auth ) 326 { 327 auth.secureRandomizer().nextBytes( randomByteArray ); 328 randomN1 = new BigInteger( /*positive*/1, randomByteArray ); 329 auth.secureRandomizer().nextBytes( randomByteArray ); 330 } 331 randomN2 = new BigInteger( /*positive*/1, randomByteArray ); 332 hashSalt1 = randomN1.toString( /*radix*/16 ); 333 hashSalt2 = randomN2.toString( /*radix*/16 ); 334 } 335 336 337 private String hashSalt1() { return hashSalt1; } 338 339 private final String hashSalt1; 340 341 342 private String hashSalt2() { return hashSalt2; } 343 344 private final String hashSalt2; 345 346 } 347 348 349}