All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openid4java.consumer.ConsumerManager Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2006-2008 Sxip Identity Corporation
 */

package org.openid4java.consumer;

import com.google.inject.Inject;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpStatus;
import org.openid4java.OpenIDException;
import org.openid4java.association.Association;
import org.openid4java.association.AssociationException;
import org.openid4java.association.AssociationSessionType;
import org.openid4java.association.DiffieHellmanSession;
import org.openid4java.discovery.Discovery;
import org.openid4java.discovery.DiscoveryException;
import org.openid4java.discovery.DiscoveryInformation;
import org.openid4java.discovery.Identifier;
import org.openid4java.discovery.yadis.YadisResolver;
import org.openid4java.message.AssociationError;
import org.openid4java.message.AssociationRequest;
import org.openid4java.message.AssociationResponse;
import org.openid4java.message.AuthFailure;
import org.openid4java.message.AuthImmediateFailure;
import org.openid4java.message.AuthRequest;
import org.openid4java.message.AuthSuccess;
import org.openid4java.message.DirectError;
import org.openid4java.message.Message;
import org.openid4java.message.MessageException;
import org.openid4java.message.ParameterList;
import org.openid4java.message.VerifyRequest;
import org.openid4java.message.VerifyResponse;
import org.openid4java.server.IncrementalNonceGenerator;
import org.openid4java.server.NonceGenerator;
import org.openid4java.server.RealmVerifier;
import org.openid4java.server.RealmVerifierFactory;
import org.openid4java.util.HttpFetcher;
import org.openid4java.util.HttpFetcherFactory;
import org.openid4java.util.HttpRequestOptions;
import org.openid4java.util.HttpResponse;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import javax.crypto.spec.DHParameterSpec;

/**
 * Manages OpenID communications with an OpenID Provider (Server).
 * 

* The Consumer site needs to have the same instance of this class throughout * the lifecycle of a OpenID authentication session. * * @author Marius Scurtescu, Johnny Bufu */ public class ConsumerManager { private static Log _log = LogFactory.getLog(ConsumerManager.class); private static final boolean DEBUG = _log.isDebugEnabled(); /** * Discovery process manager. */ private Discovery _discovery; /** * Direct pointer to HttpFetcher, for association and signature * verification requests. */ private HttpFetcher _httpFetcher; /** * Store for keeping track of the established associations. */ private ConsumerAssociationStore _associations = new InMemoryConsumerAssociationStore(); /** * Consumer-side nonce generator, needed for compatibility with OpenID 1.1. */ private NonceGenerator _consumerNonceGenerator = new IncrementalNonceGenerator(); /** * Private association store used for signing consumer nonces when operating * in compatibility (v1.x) mode. */ private ConsumerAssociationStore _privateAssociations = new InMemoryConsumerAssociationStore(); /** * Verifier for the nonces in authentication responses; * prevents replay attacks. */ private NonceVerifier _nonceVerifier = new InMemoryNonceVerifier(60); // --- association preferences --- /** * Maximum number of attmpts for establishing an association. */ private int _maxAssocAttempts = 4; /** * Flag for enabling or disabling stateless mode. */ private boolean _allowStateless = true; /** * The lowest encryption level session accepted for association sessions. */ private AssociationSessionType _minAssocSessEnc = AssociationSessionType.NO_ENCRYPTION_SHA1MAC; /** * The preferred association session type; will be attempted first. */ private AssociationSessionType _prefAssocSessEnc; /** * Parameters (modulus and generator) for the Diffie-Hellman sessions. */ private DHParameterSpec _dhParams = DiffieHellmanSession.getDefaultParameter(); /** * Timeout (in seconds) for keeping track of failed association attempts. * Default 5 minutes. */ private int _failedAssocExpire = 300; /** * Interval before the expiration of an association (in seconds) * in which the association should not be used, in order to avoid * the expiration from occurring in the middle of an authentication * transaction. Default: 300s. */ private int _preExpiryAssocLockInterval = 300; // --- authentication preferences --- /** * Flag for generating checkid_immediate authentication requests. */ private boolean _immediateAuth = false; /** * Used to perform verify realms against return_to URLs. */ private RealmVerifier _realmVerifier; /** * Instantiates a ConsumerManager with default settings. */ public ConsumerManager() { this( new RealmVerifierFactory(new YadisResolver(new HttpFetcherFactory())), new Discovery(), // uses HttpCache internally new HttpFetcherFactory()); } @Inject public ConsumerManager(RealmVerifierFactory realmFactory, Discovery discovery, HttpFetcherFactory httpFetcherFactory) { _realmVerifier = realmFactory.getRealmVerifierForConsumer(); // don't verify own (RP) identity, disable RP discovery _realmVerifier.setEnforceRpId(false); _discovery = discovery; _httpFetcher = httpFetcherFactory.createFetcher(HttpRequestOptions.getDefaultOptionsForOpCalls()); if (Association.isHmacSha256Supported()) _prefAssocSessEnc = AssociationSessionType.DH_SHA256; else _prefAssocSessEnc = AssociationSessionType.DH_SHA1; } /** * Returns discovery process manager. * * @return discovery process manager. */ public Discovery getDiscovery() { return _discovery; } /** * Sets discovery process manager. * * @param discovery discovery process manager. */ public void setDiscovery(Discovery discovery) { _discovery = discovery; } /** * Gets the association store that holds established associations with * OpenID providers. * * @see ConsumerAssociationStore */ public ConsumerAssociationStore getAssociations() { return _associations; } /** * Configures the ConsumerAssociationStore that will be used to store the * associations established with OpenID providers. * * @param associations ConsumerAssociationStore implementation * @see ConsumerAssociationStore */ @Inject public void setAssociations(ConsumerAssociationStore associations) { this._associations = associations; } /** * Gets the NonceVerifier implementation used to keep track of the nonces * that have been seen in authentication response messages. * * @see NonceVerifier */ public NonceVerifier getNonceVerifier() { return _nonceVerifier; } /** * Configures the NonceVerifier that will be used to keep track of the * nonces in the authentication response messages. * * @param nonceVerifier NonceVerifier implementation * @see NonceVerifier */ @Inject public void setNonceVerifier(NonceVerifier nonceVerifier) { this._nonceVerifier = nonceVerifier; } /** * Sets the Diffie-Hellman base parameters that will be used for encoding * the MAC key exchange. *

* If not provided the default set specified by the Diffie-Hellman algorithm * will be used. * * @param dhParams Object encapsulating modulus and generator numbers * @see DHParameterSpec DiffieHellmanSession */ public void setDHParams(DHParameterSpec dhParams) { this._dhParams = dhParams; } /** * Gets the Diffie-Hellman base parameters (modulus and generator). * * @see DHParameterSpec DiffieHellmanSession */ public DHParameterSpec getDHParams() { return _dhParams; } /** * Maximum number of attempts (HTTP calls) the RP is willing to make * for trying to establish an association with the OP. * * Default: 4; * 0 = don't use associations * * Associations and stateless mode cannot be both disabled at the same time. */ public void setMaxAssocAttempts(int maxAssocAttempts) { if (maxAssocAttempts > 0 || _allowStateless) this._maxAssocAttempts = maxAssocAttempts; else throw new IllegalArgumentException( "Associations and stateless mode " + "cannot be both disabled at the same time."); if (_maxAssocAttempts == 0) _log.info("Associations disabled."); } /** * Gets the value configured for the maximum number of association attempts * that will be performed for a given OpenID provider. *

* If an association cannot be established after this number of attempts the * ConsumerManager will fallback to stateless mode, provided the * #allowStateless preference is enabled. *

* See also: {@link #allowStateless(boolean)} {@link #statelessAllowed()} */ public int getMaxAssocAttempts() { return _maxAssocAttempts; } /** * Flag used to enable / disable the use of stateless mode. *

* Default: enabled. *

* Associations and stateless mode cannot be both disabled at the same time. * @deprecated * @see #setAllowStateless(boolean) */ public void allowStateless(boolean allowStateless) { setAllowStateless(allowStateless); } /** * Flag used to enable / disable the use of stateless mode. *

* Default: enabled. *

* Associations and stateless mode cannot be both disabled at the same time. */ public void setAllowStateless(boolean allowStateless) { if (_allowStateless || _maxAssocAttempts > 0) this._allowStateless = allowStateless; else throw new IllegalArgumentException( "Associations and stateless mode " + "cannot be both disabled at the same time."); } /** * Returns true if the ConsumerManager is configured to fallback to * stateless mode when failing to associate with an OpenID Provider. * * @deprecated * @see #isAllowStateless() */ public boolean statelessAllowed() { return _allowStateless; } /** * Returns true if the ConsumerManager is configured to fallback to * stateless mode when failing to associate with an OpenID Provider. */ public boolean isAllowStateless() { return _allowStateless; } /** * Configures the minimum level of encryption accepted for association * sessions. *

* Default: no-encryption session, SHA1 MAC association. *

* See also: {@link #allowStateless(boolean)} */ public void setMinAssocSessEnc(AssociationSessionType minAssocSessEnc) { this._minAssocSessEnc = minAssocSessEnc; } /** * Gets the minimum level of encryption that will be accepted for * association sessions. *

* Default: no-encryption session, SHA1 MAC association *

*/ public AssociationSessionType getMinAssocSessEnc() { return _minAssocSessEnc; } /** * Sets the preferred encryption type for the association sessions. *

* Default: DH-SHA256 */ public void setPrefAssocSessEnc(AssociationSessionType prefAssocSessEnc) { this._prefAssocSessEnc = prefAssocSessEnc; } /** * Gets the preferred encryption type for the association sessions. */ public AssociationSessionType getPrefAssocSessEnc() { return _prefAssocSessEnc; } /** * Sets the expiration timeout (in seconds) for keeping track of failed * association attempts. *

* If an association cannot be establish with an OP, subsequesnt * authentication request to that OP will not try to establish an * association within the timeout period configured here. *

* Default: 300s * 0 = disabled (attempt to establish an association with every * authentication request) * * @param _failedAssocExpire time in seconds to remember failed * association attempts */ public void setFailedAssocExpire(int _failedAssocExpire) { this._failedAssocExpire = _failedAssocExpire; } /** * Gets the timeout (in seconds) configured for keeping track of failed * association attempts. *

* See also: {@link #setFailedAssocExpire(int)} */ public int getFailedAssocExpire() { return _failedAssocExpire; } /** * Gets the interval before the expiration of an association * (in seconds) in which the association should not be used, * in order to avoid the expiration from occurring in the middle * of a authentication transaction. Default: 300s. */ public int getPreExpiryAssocLockInterval() { return _preExpiryAssocLockInterval; } /** * Sets the interval before the expiration of an association * (in seconds) in which the association should not be used, * in order to avoid the expiration from occurring in the middle * of a authentication transaction. Default: 300s. * * @param preExpiryAssocLockInterval The number of seconds for the * pre-expiry lock inteval. */ public void setPreExpiryAssocLockInterval(int preExpiryAssocLockInterval) { this._preExpiryAssocLockInterval = preExpiryAssocLockInterval; } /** * Configures the authentication request mode: * checkid_immediate (true) or checkid_setup (false). *

* Default: false / checkid_setup */ public void setImmediateAuth(boolean _immediateAuth) { this._immediateAuth = _immediateAuth; } /** * Returns true if the ConsumerManager is configured to attempt * checkid_immediate authentication requests. *

* Default: false */ public boolean isImmediateAuth() { return _immediateAuth; } /** * Gets the RealmVerifier used to verify realms against return_to URLs. */ public RealmVerifier getRealmVerifier() { return _realmVerifier; } /** * Sets the RealmVerifier used to verify realms against return_to URLs. */ public void setRealmVerifier(RealmVerifier realmVerifier) { this._realmVerifier = realmVerifier; } /** * Gets the max age (in seconds) configured for keeping track of nonces. *

* Nonces older than the max age will be removed from the store and * authentication responses will be considered failures. */ public int getMaxNonceAge() { return _nonceVerifier.getMaxAge(); } /** * Sets the max age (in seconds) configured for keeping track of nonces. *

* Nonces older than the max age will be removed from the store and * authentication responses will be considered failures. */ public void setMaxNonceAge(int ageSeconds) { _nonceVerifier.setMaxAge(ageSeconds); } /** * Does discovery on an identifier. It delegates the call to its * discovery manager. * * @return A List of {@link DiscoveryInformation} objects. * The list could be empty if no discovery information can * be retrieved. * * @throws DiscoveryException if the discovery process runs into errors. */ public List discover(String identifier) throws DiscoveryException { return _discovery.discover(identifier); } /** * Configures a private association store for signing consumer nonces. *

* Consumer nonces are needed to prevent replay attacks in compatibility * mode, because OpenID 1.x Providers to not attach nonces to * authentication responses. *

* One way for the Consumer to know that a consumer nonce in an * authentication response was indeed issued by itself (and thus prevent * denial of service attacks), is by signing them. * * @param associations The association store to be used for signing consumer nonces; * signing can be deactivated by setting this to null. * Signing is enabled by default. */ public void setPrivateAssociationStore(ConsumerAssociationStore associations) throws ConsumerException { if (associations == null) throw new ConsumerException( "Cannot set null private association store, " + "needed for consumer nonces."); _privateAssociations = associations; } /** * Gets the private association store used for signing consumer nonces. * * @see #setPrivateAssociationStore(ConsumerAssociationStore) */ public ConsumerAssociationStore getPrivateAssociationStore() { return _privateAssociations; } public void setConnectTimeout(int connectTimeout) { _httpFetcher.getDefaultRequestOptions() .setConnTimeout(connectTimeout); } public void setSocketTimeout(int socketTimeout) { _httpFetcher.getDefaultRequestOptions() .setSocketTimeout(socketTimeout); } public void setMaxRedirects(int maxRedirects) { _httpFetcher.getDefaultRequestOptions() .setMaxRedirects(maxRedirects); } /** * Makes a HTTP call to the specified URL with the parameters specified * in the Message. * * @param url URL endpoint for the HTTP call * @param request Message containing the parameters * @param response ParameterList that will hold the parameters received in * the HTTP response * @return the status code of the HTTP call */ private int call(String url, Message request, ParameterList response) throws MessageException { int responseCode = -1; try { if (DEBUG) _log.debug("Performing HTTP POST on " + url); HttpResponse resp = _httpFetcher.post(url, request.getParameterMap()); responseCode = resp.getStatusCode(); String postResponse = resp.getBody(); response.copyOf(ParameterList.createFromKeyValueForm(postResponse)); if (DEBUG) _log.debug("Retrived response:\n" + postResponse); } catch (IOException e) { _log.error("Error talking to " + url + " response code: " + responseCode, e); } return responseCode; } /** * Tries to establish an association with on of the service endpoints in * the list of DiscoveryInformation. *

* Iterates over the items in the discoveries parameter a maximum of * #_maxAssocAttempts times trying to esablish an association. * * @param discoveries The DiscoveryInformation list obtained by * performing dicovery on the User-supplied OpenID * identifier. Should be ordered by the priority * of the service endpoints. * @return The DiscoveryInformation instance with which * an association was established, or the one * with the highest priority if association failed. * * @see Discovery#discover(org.openid4java.discovery.Identifier) */ public DiscoveryInformation associate(List discoveries) { DiscoveryInformation discovered; Association assoc; int attemptsLeft = _maxAssocAttempts; Iterator itr = discoveries.iterator(); while (itr.hasNext() && attemptsLeft > 0) { discovered = (DiscoveryInformation) itr.next(); attemptsLeft -= associate(discovered, attemptsLeft); // check if an association was established assoc = _associations.load(discovered.getOPEndpoint().toString()); if ( assoc != null && ! Association.FAILED_ASSOC_HANDLE.equals(assoc.getHandle())) return discovered; } if (discoveries.size() > 0) { // no association established, return the first service endpoint DiscoveryInformation d0 = (DiscoveryInformation) discoveries.get(0); _log.warn("Association failed; using first entry: " + d0.getOPEndpoint()); return d0; } else { _log.error("Association attempt, but no discovey endpoints provided."); return null; } } /** * Tries to establish an association with the OpenID Provider. *

* The resulting association information will be kept on storage for later * use at verification stage. If there exists an association for the opUrl * that is not near expiration, will not construct new association. * * @param discovered DiscoveryInformation obtained during the discovery * @return The number of association attempts performed. */ private int associate(DiscoveryInformation discovered, int maxAttempts) { if (_maxAssocAttempts == 0) return 0; // associations disabled URL opUrl = discovered.getOPEndpoint(); String opEndpoint = opUrl.toString(); _log.info("Trying to associate with " + opEndpoint + " attempts left: " + maxAttempts); // check if there's an already established association Association a = _associations.load(opEndpoint); if ( a != null && (Association.FAILED_ASSOC_HANDLE.equals(a.getHandle()) || a.getExpiry().getTime() - System.currentTimeMillis() > _preExpiryAssocLockInterval * 1000) ) { _log.info("Found an existing association: " + a.getHandle()); return 0; } String handle = Association.FAILED_ASSOC_HANDLE; // build a list of association types, with the preferred one at the end LinkedHashMap requests = new LinkedHashMap(); if (discovered.isVersion2()) { requests.put(AssociationSessionType.NO_ENCRYPTION_SHA1MAC, null); requests.put(AssociationSessionType.NO_ENCRYPTION_SHA256MAC, null); requests.put(AssociationSessionType.DH_SHA1, null); requests.put(AssociationSessionType.DH_SHA256, null); } else { requests.put(AssociationSessionType.NO_ENCRYPTION_COMPAT_SHA1MAC, null); requests.put(AssociationSessionType.DH_COMPAT_SHA1, null); } if (_prefAssocSessEnc.isVersion2() == discovered.isVersion2()) requests.put(_prefAssocSessEnc, null); // build a stack of Association Request objects // and keep only the allowed by the configured preferences // the most-desirable entry is always at the top of the stack Stack reqStack = new Stack(); Iterator iter = requests.keySet().iterator(); while(iter.hasNext()) { AssociationSessionType type = (AssociationSessionType) iter.next(); // create the appropriate Association Request AssociationRequest newReq = createAssociationRequest(type, opUrl); if (newReq != null) reqStack.push(newReq); } // perform the association attempts int attemptsLeft = maxAttempts; LinkedHashMap alreadyTried = new LinkedHashMap(); while (attemptsLeft > 0 && ! reqStack.empty()) { try { attemptsLeft--; AssociationRequest assocReq = (AssociationRequest) reqStack.pop(); if (DEBUG) _log.debug("Trying association type: " + assocReq.getType()); // was this association / session type attempted already? if (alreadyTried.keySet().contains(assocReq.getType())) { if (DEBUG) _log.debug("Already tried."); continue; } // mark the current request type as already tried alreadyTried.put(assocReq.getType(), null); ParameterList respParams = new ParameterList(); int status = call(opEndpoint, assocReq, respParams); // process the response if (status == HttpStatus.SC_OK) // success response { AssociationResponse assocResp; assocResp = AssociationResponse .createAssociationResponse(respParams); // valid association response Association assoc = assocResp.getAssociation(assocReq.getDHSess()); handle = assoc.getHandle(); AssociationSessionType respType = assocResp.getType(); if ( respType.equals(assocReq.getType()) || // v1 OPs may return a success no-encryption resp ( ! discovered.isVersion2() && respType.getHAlgorithm() == null && createAssociationRequest(respType,opUrl) != null)) { // store the association and do no try alternatives _associations.save(opEndpoint, assoc); _log.info("Associated with " + discovered.getOPEndpoint() + " handle: " + assoc.getHandle()); break; } else _log.info("Discarding association response, " + "not matching consumer criteria"); } else if (status == HttpStatus.SC_BAD_REQUEST) // error response { _log.info("Association attempt failed."); // retrieve fallback sess/assoc/encryption params set by OP // and queue a new attempt AssociationError assocErr = AssociationError.createAssociationError(respParams); AssociationSessionType opType = AssociationSessionType.create( assocErr.getSessionType(), assocErr.getAssocType()); if (alreadyTried.keySet().contains(opType)) continue; // create the appropriate Association Request AssociationRequest newReq = createAssociationRequest(opType, opUrl); if (newReq != null) { if (DEBUG) _log.debug("Retrieved association type " + "from the association error: " + newReq.getType()); reqStack.push(newReq); } } } catch (OpenIDException e) { _log.error("Error encountered during association attempt.", e); } } // store OPs with which an association could not be established // so that association attempts are not performed with each auth request if (Association.FAILED_ASSOC_HANDLE.equals(handle) && _failedAssocExpire > 0) _associations.save(opEndpoint, Association.getFailedAssociation(_failedAssocExpire)); return maxAttempts - attemptsLeft; } /** * Constructs an Association Request message of the specified session and * association type, taking into account the user preferences (encryption * level, default Diffie-Hellman parameters). * * @param type The type of the association (session and association) * @param opUrl The OP for which the association request is created * @return An AssociationRequest message ready to be sent back * to the OpenID Provider, or null if an association * of the requested type cannot be built. */ private AssociationRequest createAssociationRequest( AssociationSessionType type, URL opUrl) { try { if (_minAssocSessEnc.isBetter(type)) return null; AssociationRequest assocReq = null; DiffieHellmanSession dhSess; if (type.getHAlgorithm() != null) // DH session { dhSess = DiffieHellmanSession.create(type, _dhParams); if (DiffieHellmanSession.isDhSupported(type) && Association.isHmacSupported(type.getAssociationType())) assocReq = AssociationRequest.createAssociationRequest(type, dhSess); } else if ( opUrl.getProtocol().equals("https") && // no-enc sess Association.isHmacSupported(type.getAssociationType())) assocReq = AssociationRequest.createAssociationRequest(type); if (assocReq == null) _log.warn("Could not create association of type: " + type); return assocReq; } catch (OpenIDException e) { _log.error("Error trying to create association request.", e); return null; } } /** * Builds a authentication request message for the user specified in the * discovery information provided as a parameter. *

* If the discoveries parameter contains more than one entry, it will * iterate over them trying to establish an association. If an association * cannot be established, the first entry is used with stateless mode. * * @see #associate(java.util.List) * @param discoveries The DiscoveryInformation list obtained by * performing dicovery on the User-supplied OpenID * identifier. Should be ordered by the priority * of the service endpoints. * @param returnToUrl The URL on the Consumer site where the OpenID * Provider will return the user after generating * the authentication response.
* Null if the Consumer does not with to for the * End User to be returned to it (something else * useful will have been performed via an * extension).
* Must not be null in OpenID 1.x compatibility * mode. * @return Authentication request message to be sent to the * OpenID Provider. */ public AuthRequest authenticate(List discoveries, String returnToUrl) throws ConsumerException, MessageException { return authenticate(discoveries, returnToUrl, returnToUrl); } /** * Builds a authentication request message for the user specified in the * discovery information provided as a parameter. *

* If the discoveries parameter contains more than one entry, it will * iterate over them trying to establish an association. If an association * cannot be established, the first entry is used with stateless mode. * * @see #associate(java.util.List) * @param discoveries The DiscoveryInformation list obtained by * performing dicovery on the User-supplied OpenID * identifier. Should be ordered by the priority * of the service endpoints. * @param returnToUrl The URL on the Consumer site where the OpenID * Provider will return the user after generating * the authentication response.
* Null if the Consumer does not with to for the * End User to be returned to it (something else * useful will have been performed via an * extension).
* Must not be null in OpenID 1.x compatibility * mode. * @param realm The URL pattern that will be presented to the * user when he/she will be asked to authorize the * authentication transaction. Must be a super-set * of the @returnToUrl. * @return Authentication request message to be sent to the * OpenID Provider. */ public AuthRequest authenticate(List discoveries, String returnToUrl, String realm) throws ConsumerException, MessageException { // try to associate with one OP in the discovered list DiscoveryInformation discovered = associate(discoveries); return authenticate(discovered, returnToUrl, realm); } /** * Builds a authentication request message for the user specified in the * discovery information provided as a parameter. * * @param discovered A DiscoveryInformation endpoint from the list * obtained by performing dicovery on the * User-supplied OpenID identifier. * @param returnToUrl The URL on the Consumer site where the OpenID * Provider will return the user after generating * the authentication response.
* Null if the Consumer does not with to for the * End User to be returned to it (something else * useful will have been performed via an * extension).
* Must not be null in OpenID 1.x compatibility * mode. * @return Authentication request message to be sent to the * OpenID Provider. */ public AuthRequest authenticate(DiscoveryInformation discovered, String returnToUrl) throws MessageException, ConsumerException { return authenticate(discovered, returnToUrl, returnToUrl); } /** * Builds a authentication request message for the user specified in the * discovery information provided as a parameter. * * @param discovered A DiscoveryInformation endpoint from the list * obtained by performing dicovery on the * User-supplied OpenID identifier. * @param returnToUrl The URL on the Consumer site where the OpenID * Provider will return the user after generating * the authentication response.
* Null if the Consumer does not with to for the * End User to be returned to it (something else * useful will have been performed via an * extension).
* Must not be null in OpenID 1.x compatibility * mode. * @param realm The URL pattern that will be presented to the * user when he/she will be asked to authorize the * authentication transaction. Must be a super-set * of the @returnToUrl. * @return Authentication request message to be sent to the * OpenID Provider. */ public AuthRequest authenticate(DiscoveryInformation discovered, String returnToUrl, String realm) throws MessageException, ConsumerException { if (discovered == null) throw new ConsumerException("Authentication cannot continue: " + "no discovery information provided."); Association assoc = _associations.load(discovered.getOPEndpoint().toString()); if (assoc == null) { associate(discovered, _maxAssocAttempts); assoc = _associations.load(discovered.getOPEndpoint().toString()); } String handle = assoc != null ? assoc.getHandle() : Association.FAILED_ASSOC_HANDLE; // get the Claimed ID and Delegate ID (aka OP-specific identifier) String claimedId, delegate; if (discovered.hasClaimedIdentifier()) { claimedId = discovered.getClaimedIdentifier().getIdentifier(); delegate = discovered.hasDelegateIdentifier() ? discovered.getDelegateIdentifier() : claimedId; } else { claimedId = AuthRequest.SELECT_ID; delegate = AuthRequest.SELECT_ID; } // stateless mode disabled ? if ( !_allowStateless && Association.FAILED_ASSOC_HANDLE.equals(handle)) throw new ConsumerException("Authentication cannot be performed: " + "no association available and stateless mode is disabled"); _log.info("Creating authentication request for" + " OP-endpoint: " + discovered.getOPEndpoint() + " claimedID: " + claimedId + " OP-specific ID: " + delegate); if (! discovered.isVersion2()) returnToUrl = insertConsumerNonce(discovered.getOPEndpoint().toString(), returnToUrl); AuthRequest authReq = AuthRequest.createAuthRequest(claimedId, delegate, ! discovered.isVersion2(), returnToUrl, handle, realm, _realmVerifier); authReq.setOPEndpoint(discovered.getOPEndpoint()); // ignore the immediate flag for OP-directed identifier selection if (! AuthRequest.SELECT_ID.equals(claimedId)) authReq.setImmediate(_immediateAuth); return authReq; } /** * Performs verification on the Authentication Response (assertion) * received from the OpenID Provider. *

* Three verification steps are performed: *

    *
  • nonce: the same assertion will not be accepted more * than once *
  • signatures: verifies that the message was indeed sent * by the OpenID Provider that was contacted * earlier after discovery *
  • discovered information: the information contained in the assertion * matches the one obtained during the * discovery (the OpenID Provider is * authoritative for the claimed identifier; * the received assertion is not meaningful * otherwise *
* * @param receivingUrl The URL where the Consumer (Relying Party) has * accepted the incoming message. * @param response ParameterList of the authentication response * being verified. * @param discovered Previously discovered information (which can * therefore be trusted) obtained during the discovery * phase; this should be stored and retrieved by the RP * in the user's session. * * @return A VerificationResult, containing a verified * identifier; the verified identifier is null if * the verification failed). */ public VerificationResult verify(String receivingUrl, ParameterList response, DiscoveryInformation discovered) throws MessageException, DiscoveryException, AssociationException { VerificationResult result = new VerificationResult(); _log.info("Verifying authentication response..."); // non-immediate negative response if ( "cancel".equals(response.getParameterValue("openid.mode")) ) { result.setAuthResponse(AuthFailure.createAuthFailure(response)); _log.info("Received auth failure."); return result; } // immediate negative response if ( "setup_needed".equals(response.getParameterValue("openid.mode")) || ("id_res".equals(response.getParameterValue("openid.mode")) && response.hasParameter("openid.user_setup_url") ) ) { AuthImmediateFailure fail = AuthImmediateFailure.createAuthImmediateFailure(response); result.setAuthResponse(fail); result.setOPSetupUrl(fail.getUserSetupUrl()); _log.info("Received auth immediate failure."); return result; } AuthSuccess authResp = AuthSuccess.createAuthSuccess(response); _log.info("Received positive auth response."); authResp.validate(); result.setAuthResponse(authResp); // [1/4] return_to verification if (! verifyReturnTo(receivingUrl, authResp)) { result.setStatusMsg("Return_To URL verification failed."); _log.error("Return_To URL verification failed."); return result; } // [2/4] : discovered info verification discovered = verifyDiscovered(authResp, discovered); if (discovered == null || ! discovered.hasClaimedIdentifier()) { result.setStatusMsg("Discovered information verification failed."); _log.error("Discovered information verification failed."); return result; } // [3/4] : nonce verification if (! verifyNonce(authResp, discovered)) { result.setStatusMsg("Nonce verification failed."); _log.error("Nonce verification failed."); return result; } // [4/4] : signature verification return (verifySignature(authResp, discovered, result)); } /** * Verifies that the URL where the Consumer (Relying Party) received the * authentication response matches the value of the "openid.return_to" * parameter in the authentication response. * * @param receivingUrl The URL where the Consumer received the * authentication response. * @param response The authentication response. * @return True if the two URLs match, false otherwise. */ public boolean verifyReturnTo(String receivingUrl, AuthSuccess response) { if (DEBUG) _log.debug("Verifying return URL; receiving: " + receivingUrl + "\nmessage: " + response.getReturnTo()); URL receiving; URL returnTo; try { receiving = new URL(receivingUrl); returnTo = new URL(response.getReturnTo()); } catch (MalformedURLException e) { _log.error("Invalid return URL.", e); return false; } // [1/2] schema, authority (includes port) and path // deal manually with the trailing slash in the path StringBuffer receivingPath = new StringBuffer(receiving.getPath()); if ( receivingPath.length() > 0 && receivingPath.charAt(receivingPath.length() -1) != '/') receivingPath.append('/'); StringBuffer returnToPath = new StringBuffer(returnTo.getPath()); if ( returnToPath.length() > 0 && returnToPath.charAt(returnToPath.length() -1) != '/') returnToPath.append('/'); if ( ! receiving.getProtocol().equals(returnTo.getProtocol()) || ! receiving.getAuthority().equals(returnTo.getAuthority()) || ! receivingPath.toString().equals(returnToPath.toString()) ) { if (DEBUG) _log.debug("Return URL schema, authority or " + "path verification failed."); return false; } // [2/2] query parameters try { Map returnToParams = extractQueryParams(returnTo); Map receivingParams = extractQueryParams(receiving); if (returnToParams == null) return true; if (receivingParams == null) { if (DEBUG) _log.debug("Return URL query parameters verification failed."); return false; } Iterator iter = returnToParams.keySet().iterator(); while (iter.hasNext()) { String key = (String) iter.next(); List receivingValues = (List) receivingParams.get(key); List returnToValues = (List) returnToParams.get(key); if ( receivingValues == null || receivingValues.size() != returnToValues.size() || ! receivingValues.containsAll( returnToValues ) ) { if (DEBUG) _log.debug("Return URL query parameters verification failed."); return false; } } } catch (UnsupportedEncodingException e) { _log.error("Error verifying return URL query parameters.", e); return false; } return true; } /** * Returns a Map(key, List(values)) with the URL's query params, or null if * the URL doesn't have a query string. */ public Map extractQueryParams(URL url) throws UnsupportedEncodingException { if (url.getQuery() == null) return null; Map paramsMap = new HashMap(); List paramList = Arrays.asList(url.getQuery().split("&")); Iterator iter = paramList.iterator(); while (iter.hasNext()) { String keyValue = (String) iter.next(); int equalPos = keyValue.indexOf("="); String key = equalPos > -1 ? URLDecoder.decode(keyValue.substring(0, equalPos), "UTF-8") : URLDecoder.decode(keyValue, "UTF-8"); String value; if (equalPos <= -1) value = null; else if (equalPos + 1 > keyValue.length()) value = ""; else value = URLDecoder.decode(keyValue.substring(equalPos + 1), "UTF-8"); List existingValues = (List) paramsMap.get(key); if (existingValues == null) { List newValues = new ArrayList(); newValues.add(value); paramsMap.put(key, newValues); } else existingValues.add(value); } return paramsMap; } /** * Verifies the nonce in an authentication response. * * @param authResp The authentication response containing the nonce * to be verified. * @param discovered The discovery information associated with the * authentication transaction. * @return True if the nonce is valid, false otherwise. */ public boolean verifyNonce(AuthSuccess authResp, DiscoveryInformation discovered) { String nonce = authResp.getNonce(); if (nonce == null) // compatibility mode nonce = extractConsumerNonce(authResp.getReturnTo(), discovered.getOPEndpoint().toString()); if (nonce == null) return false; // using the same nonce verifier for both server and consumer nonces return (NonceVerifier.OK == _nonceVerifier.seen( discovered.getOPEndpoint().toString(), nonce)); } /** * Inserts a consumer-side nonce as a custom parameter in the return_to * parameter of the authentication request. *

* Needed for preventing replay attack when running compatibility mode. * OpenID 1.1 OpenID Providers do not generate nonces in authentication * responses. * * @param opUrl The endpoint to be used for private association. * @param returnTo The return_to URL to which a custom nonce * parameter will be added. * @return The return_to URL containing the nonce. */ public String insertConsumerNonce(String opUrl, String returnTo) { String nonce = _consumerNonceGenerator.next(); returnTo += (returnTo.indexOf('?') != -1) ? '&' : '?'; Association privateAssoc = _privateAssociations.load(opUrl); if( privateAssoc == null ) { try { if (DEBUG) _log.debug( "Creating private association for opUrl " + opUrl); privateAssoc = Association.generate( getPrefAssocSessEnc().getAssociationType(), "", _failedAssocExpire); _privateAssociations.save( opUrl, privateAssoc ); } catch ( AssociationException e ) { _log.error("Cannot initialize private association.", e); return null; } } try { returnTo += "openid.rpnonce=" + URLEncoder.encode(nonce, "UTF-8"); returnTo += "&openid.rpsig=" + URLEncoder.encode(privateAssoc.sign(returnTo), "UTF-8"); _log.info("Inserted consumer nonce: " + nonce); if (DEBUG) _log.debug("return_to:" + returnTo); } catch (Exception e) { _log.error("Error inserting consumre nonce.", e); return null; } return returnTo; } /** * Extracts the consumer-side nonce from the return_to parameter in * authentication response from a OpenID 1.1 Provider. * * @param returnTo return_to URL from the authentication response * @param opUrl URL for the appropriate OP endpoint * @return The nonce found in the return_to URL, or null if * it wasn't found. */ public String extractConsumerNonce(String returnTo, String opUrl) { if (DEBUG) _log.debug("Extracting consumer nonce..."); String nonce = null; String signature = null; URL returnToUrl; try { returnToUrl = new URL(returnTo); } catch (MalformedURLException e) { _log.error("Invalid return_to: " + returnTo, e); return null; } String query = returnToUrl.getQuery(); String[] params = query.split("&"); for (int i=0; i < params.length; i++) { String keyVal[] = params[i].split("=", 2); try { if (keyVal.length == 2 && "openid.rpnonce".equals(keyVal[0])) { nonce = URLDecoder.decode(keyVal[1], "UTF-8"); if (DEBUG) _log.debug("Extracted consumer nonce: " + nonce); } if (keyVal.length == 2 && "openid.rpsig".equals(keyVal[0])) { signature = URLDecoder.decode(keyVal[1], "UTF-8"); if (DEBUG) _log.debug("Extracted consumer nonce signature: " + signature); } } catch (UnsupportedEncodingException e) { _log.error("Error extracting consumer nonce / signarure.", e); return null; } } // check the signature if (signature == null) { _log.error("Null consumer nonce signature."); return null; } String signed = returnTo.substring(0, returnTo.indexOf("&openid.rpsig=")); if (DEBUG) _log.debug("Consumer signed text:\n" + signed); try { if (DEBUG) _log.debug( "Loading private association for opUrl " + opUrl ); Association privateAssoc = _privateAssociations.load(opUrl); if( privateAssoc == null ) { _log.error("Null private association."); return null; } if (privateAssoc.verifySignature(signed, signature)) { _log.info("Consumer nonce signature verified."); return nonce; } else { _log.error("Consumer nonce signature failed."); return null; } } catch (AssociationException e) { _log.error("Error verifying consumer nonce signature.", e); return null; } } /** * Verifies the dicovery information matches the data received in a * authentication response from an OpenID Provider. * * @param authResp The authentication response to be verified. * @param discovered The discovery information obtained earlier during * the discovery stage, associated with the * identifier(s) in the request. Stateless operation * is assumed if null. * @return The discovery information associated with the * claimed identifier, that can be used further in * the verification process. Null if the discovery * on the claimed identifier does not match the data * in the assertion. */ private DiscoveryInformation verifyDiscovered(AuthSuccess authResp, DiscoveryInformation discovered) throws DiscoveryException { if (authResp == null || authResp.getIdentity() == null) { _log.info("Assertion is not about an identifier"); return null; } if (authResp.isVersion2()) return verifyDiscovered2(authResp, discovered); else return verifyDiscovered1(authResp, discovered); } /** * Verifies the discovered information associated with a OpenID 1.x * response. * * @param authResp The authentication response to be verified. * @param discovered The discovery information obtained earlier during * the discovery stage, associated with the * identifier(s) in the request. Stateless operation * is assumed if null. * @return The discovery information associated with the * claimed identifier, that can be used further in * the verification process. Null if the discovery * on the claimed identifier does not match the data * in the assertion. */ private DiscoveryInformation verifyDiscovered1(AuthSuccess authResp, DiscoveryInformation discovered) throws DiscoveryException { if ( authResp == null || authResp.isVersion2() || authResp.getIdentity() == null ) { if (DEBUG) _log.error("Invalid authentication response: " + "cannot verify v1 discovered information"); return null; } // asserted identifier in the AuthResponse String assertId = authResp.getIdentity(); if ( discovered != null && ! discovered.isVersion2() && discovered.getClaimedIdentifier() != null ) { // statefull mode if (DEBUG) _log.debug("Verifying discovered information " + "for OpenID1 assertion about ClaimedID: " + discovered.getClaimedIdentifier().getIdentifier()); String discoveredId = discovered.hasDelegateIdentifier() ? discovered.getDelegateIdentifier() : discovered.getClaimedIdentifier().getIdentifier(); if (assertId.equals(discoveredId)) return discovered; } // stateless, bare response, or the user changed the ID at the OP _log.info("Proceeding with stateless mode / bare response verification..."); DiscoveryInformation firstServiceMatch = null; // assuming openid.identity is the claimedId // (delegation can't work with stateless/bare resp v1 operation) if (DEBUG) _log.debug( "Performing discovery on the ClaimedID in the assertion: " + assertId); List discoveries = _discovery.discover(assertId); Iterator iter = discoveries.iterator(); while (iter.hasNext()) { DiscoveryInformation service = (DiscoveryInformation) iter.next(); if (service.isVersion2() || // only interested in v1 ! service.hasClaimedIdentifier() || // need a claimedId service.hasDelegateIdentifier() || // not allowing delegates ! assertId.equals(service.getClaimedIdentifier().getIdentifier())) continue; if (DEBUG) _log.debug("Found matching service: " + service); // keep the first endpoint that matches if (firstServiceMatch == null) firstServiceMatch = service; Association assoc = _associations.load( service.getOPEndpoint().toString(), authResp.getHandle()); // don't look further if there is an association with this endpoint if (assoc != null) { if (DEBUG) _log.debug("Found existing association for " + service + " Not looking for another service endpoint."); return service; } } if (firstServiceMatch == null) _log.error("No service element found to match " + "the identifier in the assertion."); return firstServiceMatch; } /** * Verifies the discovered information associated with a OpenID 2.0 * response. * * @param authResp The authentication response to be verified. * @param discovered The discovery information obtained earlier during * the discovery stage, associated with the * identifier(s) in the request. Stateless operation * is assumed if null. * @return The discovery information associated with the * claimed identifier, that can be used further in * the verification process. Null if the discovery * on the claimed identifier does not match the data * in the assertion. */ private DiscoveryInformation verifyDiscovered2(AuthSuccess authResp, DiscoveryInformation discovered) throws DiscoveryException { if (authResp == null || ! authResp.isVersion2() || authResp.getIdentity() == null || authResp.getClaimed() == null) { if (DEBUG) _log.debug("Discovered information doesn't match " + "auth response / version"); return null; } // asserted identifier in the AuthResponse String assertId = authResp.getIdentity(); // claimed identifier in the AuthResponse Identifier respClaimed = _discovery.parseIdentifier(authResp.getClaimed(), true); // the OP endpoint sent in the response String respEndpoint = authResp.getOpEndpoint(); if (DEBUG) _log.debug("Verifying discovered information for OpenID2 assertion " + "about ClaimedID: " + respClaimed.getIdentifier()); // was the claimed identifier in the assertion previously discovered? if (discovered != null && discovered.hasClaimedIdentifier() && discovered.getClaimedIdentifier().equals(respClaimed) ) { // OP-endpoint, OP-specific ID and protocol version must match String opSpecific = discovered.hasDelegateIdentifier() ? discovered.getDelegateIdentifier() : discovered.getClaimedIdentifier().getIdentifier(); if ( opSpecific.equals(assertId) && discovered.isVersion2() && discovered.getOPEndpoint().toString().equals(respEndpoint)) { if (DEBUG) _log.debug( "ClaimedID in the assertion was previously discovered: " + respClaimed); return discovered; } } // stateless, bare response, or the user changed the ID at the OP DiscoveryInformation firstServiceMatch = null; // perform discovery on the claim identifier in the assertion if(DEBUG) _log.debug( "Performing discovery on the ClaimedID in the assertion: " + respClaimed); List discoveries = _discovery.discover(respClaimed); // find the newly discovered service endpoint that matches the assertion // - OP endpoint, OP-specific ID and protocol version must match // - prefer (first = highest priority) endpoint with an association if (DEBUG) _log.debug("Looking for a service element to match " + "the ClaimedID and OP endpoint in the assertion..."); Iterator iter = discoveries.iterator(); while (iter.hasNext()) { DiscoveryInformation service = (DiscoveryInformation) iter.next(); if (DiscoveryInformation.OPENID2_OP.equals(service.getVersion())) continue; String opSpecific = service.hasDelegateIdentifier() ? service.getDelegateIdentifier() : service.getClaimedIdentifier().getIdentifier(); if ( ! opSpecific.equals(assertId) || ! service.isVersion2() || ! service.getOPEndpoint().toString().equals(respEndpoint) ) continue; // keep the first endpoint that matches if (firstServiceMatch == null) { if (DEBUG) _log.debug("Found matching service: " + service); firstServiceMatch = service; } Association assoc = _associations.load( service.getOPEndpoint().toString(), authResp.getHandle()); // don't look further if there is an association with this endpoint if (assoc != null) { if (DEBUG) _log.debug("Found existing association, " + "not looking for another service endpoint."); return service; } } if (firstServiceMatch == null) _log.error("No service element found to match " + "the ClaimedID / OP-endpoint in the assertion."); return firstServiceMatch; } /** * Verifies the signature in a authentication response message. * * @param authResp Authentication response to be verified. * @param discovered The discovery information obtained earlier during * the discovery stage. * @return True if the verification succeeded, false otherwise. */ private VerificationResult verifySignature(AuthSuccess authResp, DiscoveryInformation discovered, VerificationResult result) throws AssociationException, MessageException, DiscoveryException { if (discovered == null || authResp == null) { _log.error("Can't verify signature: " + "null assertion or discovered information."); result.setStatusMsg("Can't verify signature: " + "null assertion or discovered information."); return result; } Identifier claimedId = discovered.isVersion2() ? _discovery.parseIdentifier(authResp.getClaimed()) : //may have frag discovered.getClaimedIdentifier(); //assert id may be delegate in v1 String handle = authResp.getHandle(); URL op = discovered.getOPEndpoint(); Association assoc = _associations.load(op.toString(), handle); if (assoc != null) // association available, local verification { _log.info("Found association: " + assoc.getHandle() + " verifying signature locally..."); String text = authResp.getSignedText(); String signature = authResp.getSignature(); if (assoc.verifySignature(text, signature)) { result.setVerifiedId(claimedId); if (DEBUG) _log.debug("Local signature verification succeeded."); } else if (DEBUG) { _log.debug("Local signature verification failed."); result.setStatusMsg("Local signature verification failed"); } } else // no association, verify with the OP { _log.info("No association found, " + "contacting the OP for direct verification..."); VerifyRequest vrfy = VerifyRequest.createVerifyRequest(authResp); ParameterList responseParams = new ParameterList(); int respCode = call(op.toString(), vrfy, responseParams); if (HttpStatus.SC_OK == respCode) { VerifyResponse vrfyResp = VerifyResponse.createVerifyResponse(responseParams); vrfyResp.validate(); if (vrfyResp.isSignatureVerified()) { // process the optional invalidate_handle first String invalidateHandle = vrfyResp.getInvalidateHandle(); if (invalidateHandle != null) _associations.remove(op.toString(), invalidateHandle); result.setVerifiedId(claimedId); if (DEBUG) _log.debug("Direct signature verification succeeded " + "with OP: " + op); } else { if (DEBUG) _log.debug("Direct signature verification failed " + "with OP: " + op); result.setStatusMsg("Direct signature verification failed."); } } else { DirectError err = DirectError.createDirectError(responseParams); if (DEBUG) _log.debug("Error verifying signature with the OP: " + op + " error message: " + err.keyValueFormEncoding()); result.setStatusMsg("Error verifying signature with the OP: " + err.getErrorMsg()); } } Identifier verifiedID = result.getVerifiedId(); if (verifiedID != null) _log.info("Verification succeeded for: " + verifiedID); else _log.error("Verification failed for: " + authResp.getClaimed() + " reason: " + result.getStatusMsg()); return result; } /* visible for testing */ HttpFetcher getHttpFetcher() { return _httpFetcher; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy