package 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. import java.io.*; import java.math.*; import java.security.*; import java.sql.SQLException; import java.util.logging.*; import javax.mail.internet.*; import javax.servlet.http.*; import org.apache.wicket.Session; import org.apache.wicket.ISessionListener; import org.apache.wicket.request.Request; import org.apache.wicket.request.http.WebResponse; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.openid4java.consumer.*; import votorola.a.voter.*; import votorola.a.web.wic.*; import votorola.g.io.*; import votorola.g.lang.*; import votorola.g.logging.*; import votorola.g.web.CookieX; import votorola.g.web.wic.*; /** An authenticator based on OpenID and email handshakes. */ public @ThreadSafe final class OpenIDAuthenticator extends Authenticator implements ISessionListener { /** Creates an OpenIDAuthenticator. */ public OpenIDAuthenticator( VOWicket _app ) { app = _app; persistenceFile = new File( app.vsRun().voteServer().outDirectory(), "login-persistence.serial" ); if( persistenceFile.isFile() ) { try{ persistence = (Persistence)FileX.readObject( persistenceFile ); } catch( ClassNotFoundException|IOException x ) { logger.config( "Unable to recreate persistence from previous run. Creating anew. Old persisted logins will be lost. Reason: " + x.toString() ); if( !persistenceFile.delete() ) recreatePersistence(); } } app.getSessionListeners().add( OpenIDAuthenticator.this ); // no need to unregister, registry does not outlive this listener } // ------------------------------------------------------------------------------------ /** The name of the cookie that stores the login key of the user if login is * cookie-persisted for that user. * * @see #newPersistKey(String,String) */ static final String COOKIE_PERSIST_KEY = "vo_loginPersistKey"; // "voLogin.persistKey" would have been more standard /** The name of the cookie that stores the email address of the user if login is * cookie-persisted for that user. */ static final String COOKIE_PERSIST_USER_EMAIL = "vo_loginPersistUserEmail"; // "voLogin.persistUserEmail" would have been more standard /** The OpenID consumer manager. */ @ThreadRestricted("holds OpenIDAuthenticator.this") ConsumerManager consumerManager() { assert Thread.holdsLock( OpenIDAuthenticator.this ); // actually it's the object that is restricted, not this method return consumerManager; } private ConsumerManager consumerManager = new ConsumerManager(); /** Constructs a login persistence cookie. */ static Cookie newPersistCookie( final String name, final String value, final boolean toEncode, final Request req ) { final Cookie cookie = new CookieX( name, value, toEncode ); cookie.setMaxAge( COOKIE_PERSIST_DURATION_S ); cookie.setPath( req.getContextPath() ); // allow access from pages other than WP_OpenIDLogin return cookie; } /** Calculates a secure hash of the user's login fingerprint and returns it as a * string in radix 36. * * @param ipAddress the IP address of the client or proxy. */ String newPersistKey( final String userEmail, final String ipAddress ) { if( !persistenceFile.isFile() ) recreatePersistence(); final String fingerprint = persistence.hashSalt1() + "/" + userEmail + "/" + ipAddress + "/" + persistence.hashSalt2(); try { final MessageDigest digester = MessageDigest.getInstance( "SHA-256" ); final byte[] digest = digester.digest( fingerprint.getBytes() ); final BigInteger digestNumber = new BigInteger( /*positive*/1, digest ); return digestNumber.toString( /*radix*/36 ).toUpperCase(); } catch( NoSuchAlgorithmException x ) { throw new RuntimeException( x ); } } /** The secure random number generator. */ @ThreadRestricted("holds OpenIDAuthenticator.this") SecureRandom secureRandomizer() { assert Thread.holdsLock( OpenIDAuthenticator.this ); // actually it's the object that is restricted, not this method return secureRandomizer; } private final SecureRandom secureRandomizer = new SecureRandom(); // - A u t h e n t i c a t o r -------------------------------------------------------- public Class loginPageClass() { return WP_OpenIDLogin.class; } public void logOut() { final VRequestCycle cycle = VRequestCycle.get(); final VSession session = VSession.get(); final VSession.User user = session.user(); if( user != null ) WP_OpenIDLogin.clearPersistence( user.email(), cycle ); session.clearUser( cycle ); } public VPageHTML newLoginPage( PageParameters _pP ) { return new WP_OpenIDLogin( _pP ); } // - I - S e s s i o n - L i s t e n e r ---------------------------------------------- public void onCreated( final Session s ) { final VRequestCycle cycle = VRequestCycle.get(); final Request req = cycle.getRequest(); IDPair persistedUser = persistedUser( (HttpServletRequest)req.getContainerRequest(), app, req, (WebResponse)cycle.getResponse() ); final boolean isPersistent; if( persistedUser == null ) { if( PRELOGIN_EMAIL == null ) return; persistedUser = IDPair.fromEmail( PRELOGIN_EMAIL ); isPersistent = false; } else isPersistent = true; final VSession session = (VSession)s; try { session.setUser( persistedUser, isPersistent, app.vsRun().trustserver().getTraceNode(/*list ref*/null,persistedUser), cycle ); } catch( IOException|SQLException x ) { throw new RuntimeException( x ); } } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private final VOWicket app; // private IDPair apparentlyPersistedUser( HttpServletRequest _reqHS ) // { // return persistedUser( _reqHS, null, null ); // } /** The maximum duration of a cookie-persisted login for an inactive user, in seconds. * Cookies are refreshed whenever the user is active during this time, thus * effectively resetting the clock. */ private static final int COOKIE_PERSIST_DURATION_S = 86400/*s per day*/ * 14/*day*/; private static final Logger logger = LoggerX.i( OpenIDAuthenticator.class ); /** Extracts and verifies the identifier of the persistently authenticated user. * Returns either the verified identifier, or null if well-formed persistence data * cannot be retrieved from the request cookies, or if verification fails. * * @param authenAppOrNull null to skip authentication and refresh of cookies. * @param reqOrNull may be null only if authenAppOrNull is null. * @param resWOrNull the web response in which the persistence data is to be * refreshed, if necessary. May be null only if authenAppOrNull is null. */ private static IDPair persistedUser( final HttpServletRequest reqHS, final VOWicket authenAppOrNull, final Request reqOrNull, final WebResponse resWOrNull ) { // http://stackoverflow.com/questions/1354999/keep-me-logged-in-the-best-approach assert !(reqOrNull == null || resWOrNull == null) || authenAppOrNull == null; final Cookie[] cookies = reqHS.getCookies(); if( cookies == null ) return null; String userEmail = null; String key = null; for( final Cookie cookie: cookies ) { final String c = cookie.getName(); if( c.equals( COOKIE_PERSIST_USER_EMAIL )) { userEmail = CookieX.decodedValue( cookie.getValue() ); if( key != null ) break; // that's both of them } else if( c.equals( COOKIE_PERSIST_KEY )) { key = cookie.getValue(); if( userEmail != null ) break; // that's both of them } } if( key == null || userEmail == null ) return null; try{ new InternetAddress( userEmail, /*strict*/true ); } catch( AddressException x ) { logger.log( LoggerX.INFO, "ignoring invalid email address in cookie", x ); return null; } final IDPair user = IDPair.fromEmail( userEmail ); if( authenAppOrNull != null ) { final OpenIDAuthenticator auth = (OpenIDAuthenticator)authenAppOrNull.authenticator(); final String expectedKey = auth.newPersistKey( userEmail, reqHS.getRemoteAddr() ); if( !expectedKey.equals( key )) return null; // user IP changed, or persistence salts file recreated, or phoney key try { final UserSettings settings = new UserSettings( userEmail, authenAppOrNull.vsRun().userTable() ); if( !expectedKey.equals( settings.getLoginPersistKey() )) return null; // user never originally authenticated, or settings table cleared } catch( SQLException x ) { logger.log( LoggerX.WARNING, "unable to authenticate persistent login", x ); return null; } // refresh cookies // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` WebResponseX.addCookie( resWOrNull, newPersistCookie( COOKIE_PERSIST_USER_EMAIL, userEmail, /*toEncode*/true, reqOrNull )); WebResponseX.addCookie( resWOrNull, newPersistCookie( COOKIE_PERSIST_KEY, expectedKey, /*toEncode*/false, reqOrNull )); } return user; } private volatile Persistence persistence; private final File persistenceFile; private @Warning("init call") void recreatePersistence() { persistence = new Persistence( OpenIDAuthenticator.this ); try { FileX.writeObject( persistence, persistenceFile ); } catch( IOException x ) { logger.warning( "unable to store new persistence to file: " + x.toString() ); } persistenceFile.setReadable( false, /*ownerOnly*/false ); // nobody can read/write persistenceFile.setWritable( false, /*ownerOnly*/false ); persistenceFile.setReadable( true, /*ownerOnly*/true ); // only owner can read/write persistenceFile.setWritable( true, /*ownerOnly*/true ); } // ==================================================================================== /** Parameters required for login persistence. */ private static final class Persistence implements Serializable { private static final long serialVersionUID = 0L; Persistence( final OpenIDAuthenticator auth ) { final byte[] randomByteArray = new byte[2]; // raw format final BigInteger randomN1, randomN2; synchronized( auth ) { auth.secureRandomizer().nextBytes( randomByteArray ); randomN1 = new BigInteger( /*positive*/1, randomByteArray ); auth.secureRandomizer().nextBytes( randomByteArray ); } randomN2 = new BigInteger( /*positive*/1, randomByteArray ); hashSalt1 = randomN1.toString( /*radix*/16 ); hashSalt2 = randomN2.toString( /*radix*/16 ); } private String hashSalt1() { return hashSalt1; } private final String hashSalt1; private String hashSalt2() { return hashSalt2; } private final String hashSalt2; } }