package org.verisign.joid.consumer; import org.verisign.joid.*; import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.Log; import java.io.IOException; import java.math.BigInteger; import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.Map; import java.util.Properties; /** * This is the main class for consumers to use. * <p/> * It performs the following operations given an OpenID user identifier. * - Finds the OpenId Server * - Associates with the server if it hasn't done so already or if the * association has expired * - Provides url to the server an application to redirect to. * <p/> * ... some time later ... * <p/> * - Takes a request from an OpenId server after user has authenticated * - Verifies server signature and our signatures match to authenticate * - Returns the user's identifier if ok * <p/> * <p/> * User: treeder * Date: Jun 27, 2007 * Time: 11:52:40 AM */ public class JoidConsumer { private static Log log = LogFactory.getLog(JoidConsumer.class); private Map/*<String, Properties>*/ propSingleton; private Map/*<String, String>*/ handleToIdServer; private Discoverer discoverer = new Discoverer(); public JoidConsumer() { log.info("Constructor: JoidConsumer"); } private synchronized Properties getProps(String idserver) { // todo: just store the AssociationResponse instead of converting to props if (propSingleton == null) { propSingleton = new HashMap(); handleToIdServer = new HashMap(); } Properties props = (Properties) propSingleton.get(idserver); if (props == null) { // todo: also check expires_in time to make sure it's still valid try { props = associate(idserver); propSingleton.put(idserver, props); handleToIdServer.put(props.getProperty("handle"), idserver); } catch (Exception e) { e.printStackTrace(); } } return props; } private Properties getPropsByHandle(String associationHandle) throws OpenIdException { String idServer = (String) handleToIdServer.get(associationHandle); log.info("got idserver for handle: " + associationHandle + " - " + idServer); if (idServer == null) { throw new OpenIdException("handle for server not found!"); } return getProps(idServer); } /** * To associate with an openid server * * @param idserver server url * @return * @throws java.io.IOException * @throws org.verisign.joid.OpenIdException * */ public Properties associate(String idserver) throws IOException, OpenIdException { DiffieHellman dh = DiffieHellman.getDefault(); Crypto crypto = new Crypto(); crypto.setDiffieHellman(dh); AssociationRequest ar = AssociationRequest.create(crypto); log.info("[JoidConsumer] Attempting to associate with: " + idserver); log.info("Request=" + ar); Response response = Util.send(ar, idserver); log.info("Response=" + response + "\n"); AssociationResponse asr = (AssociationResponse) response; Properties props = new Properties(); props.setProperty("idServer", idserver); props.setProperty("handle", asr.getAssociationHandle()); props.setProperty("publicKey", Crypto.convertToString(asr.getDhServerPublic())); props.setProperty("encryptedKey", Crypto.convertToString(asr.getEncryptedMacKey())); BigInteger privateKey = dh.getPrivateKey(); props.setProperty("privateKey", Crypto.convertToString(privateKey)); props.setProperty("modulus", Crypto.convertToString(DiffieHellman.DEFAULT_MODULUS)); props.setProperty("_dest", idserver); props.setProperty("expiresIn", "" + asr.getExpiresIn()); /* Crypto crypto = new Crypto(); dh = DiffieHellman.recreate(privateKey, p); crypto.setDiffieHellman(dh); byte[] clearKey = crypto.decryptSecret(asr.getDhServerPublic(), asr.getEncryptedMacKey()); System.out.println("Clear key: "+Crypto.convertToString(clearKey)); */ return props; } /** * <p> * This method is used by a relying party to create the url to redirect a * user to after entering their OpenId URL in a form. * </p> * <p> * It will find the id server found at the OpenID url, associate with the * server if necessary and return an authentication request url. * </p> * * @param identity users OpenID url * @param returnTo the url to return to after user is finished with OpenId provider * @param trustRoot base url that the authentication should apply to * @return * @throws OpenIdException */ public String getAuthUrl(String identity, String returnTo, String trustRoot) throws OpenIdException { // find id server ServerAndDelegate idserver = null; try { idserver = discoverer.findIdServer(identity); } catch (Exception e) { e.printStackTrace(); throw new OpenIdException("Could not get OpenId server from " + "identifier.", e); } Properties p = getProps(idserver.getServer()); String handle = p.getProperty("handle"); // todo: use delegate here, replace identity? AuthenticationRequest ar = AuthenticationRequest.create(identity, returnTo, trustRoot, handle); log.info("urlString=" + ar.toUrlString()); return idserver.getServer() + "?" + ar.toUrlString(); } /** * This method will attempt to authenticate against the OpenID server. * * @param map * @return openid.identity if authentication was successful, null if unsuccessful * @throws IOException * @throws OpenIdException * @throws NoSuchAlgorithmException */ public AuthenticationResult authenticate(Map map) throws IOException, OpenIdException, NoSuchAlgorithmException { log.debug("request map in authenticate: " + map); AuthenticationResponse response = new AuthenticationResponse(map); // todo: store nonce's to ensure we never accept the same value again - see sec 11.3 of spec 2.0 Properties props; if (response.getInvalidateHandle() != null) { // then we have to send a authentication_request (dumb mode) to verify the signature CheckAuthenticationRequest checkReq = new CheckAuthenticationRequest(response.toMap(), RequestFactory.CHECK_AUTHENTICATION_MODE); props = getPropsByHandle(response.getInvalidateHandle()); CheckAuthenticationResponse response2 = (CheckAuthenticationResponse) Util.send(checkReq, props.getProperty("idServer")); // todo: verify the invalidate_handle in response2 is the same as in response removeInvalidHandle(response.getInvalidateHandle()); if (response2.isValid()) { // then this is a valid request, lets send it back return new AuthenticationResult(response.getIdentity(), response); } else { throw new AuthenticationException("Signature invalid, identity denied."); } } else { // normal properties props = getPropsByHandle(response.getAssociationHandle()); // todo: before returning a valid response, ensure return_to is a suburl of trust_root BigInteger privKey = Crypto.convertToBigIntegerFromString(props.getProperty("privateKey")); BigInteger modulus = Crypto.convertToBigIntegerFromString(props.getProperty("modulus")); BigInteger serverPublic = Crypto.convertToBigIntegerFromString(props.getProperty("publicKey")); byte[] encryptedKey = Crypto.convertToBytes(props.getProperty("encryptedKey")); /* String sig = response.sign(response.getAssociationType(), a.getMacKey(), response.getSignedList()); isValid = sig.equals(response.getSignature()); */ DiffieHellman dh = DiffieHellman.recreate(privKey, modulus); Crypto crypto = new Crypto(); crypto.setDiffieHellman(dh); byte[] clearKey = crypto.decryptSecret(serverPublic, encryptedKey); String signature = response.getSignature(); log.info("Server's signature: " + signature); String sigList = response.getSignedList(); String reSigned = response.sign(clearKey, sigList); log.info("Our signature: " + reSigned); String identity = response.getIdentity(); if (!signature.equals(reSigned)) { throw new AuthenticationException("OpenID signatures do not match! " + "claimed identity: " + identity); } log.info("Signatures match, identity is ok: " + identity); return new AuthenticationResult(identity, response); } } /** * If openid.invalidate_handle was received, this will remove it from our * cache so it won't be used again. * * @param invalidateHandle */ private void removeInvalidHandle(String invalidateHandle) { String idServer = (String) handleToIdServer.remove(invalidateHandle); if (idServer != null) { propSingleton.remove(idServer); } } }