/*
* Copyright 2008 Google, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.oauth;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.apache.log4j.Logger;
import net.oauth.signature.OAuthSignatureMethod;
//TODO: move this class into oauth-provider
/**
* A simple OAuthValidator, which checks the version, whether the timestamp is
* close to now, the nonce hasn't been used before and the signature is valid.
* Each check may be overridden.
*
* This implementation is less than industrial strength:
*
* - Duplicate nonces won't be reliably detected by a service provider running
* in multiple processes, since the used nonces are stored in memory.
* - The collection of used nonces is a synchronized choke point
* - The used nonces may occupy lots of memory, although you can minimize this
* by calling releaseGarbage periodically.
* - The range of acceptable timestamps can't be changed, and there's no
* system for increasing the range smoothly.
* - Correcting the clock backward may allow duplicate nonces.
*
* For a big service provider, it might be better to store used nonces in a
* database.
*
* @author Dirk Balfanz
* @author John Kristian
*/
public class SimpleOAuthValidator implements OAuthValidator {
/** The default maximum age of timestamps is 5 minutes. */
public static final long DEFAULT_MAX_TIMESTAMP_AGE = 5 * 60 * 1000L;
public static final long DEFAULT_TIMESTAMP_WINDOW = DEFAULT_MAX_TIMESTAMP_AGE;
private static Logger log = Logger.getLogger(SimpleOAuthValidator.class);
/**
* Names of parameters that may not appear twice in a valid message.
* This limitation is specified by OAuth Core section 5.
*/
public static final Set SINGLE_PARAMETERS = constructSingleParameters();
private static Set constructSingleParameters() {
Set s = new HashSet();
for (String p : new String[] { OAuth.OAUTH_CONSUMER_KEY, OAuth.OAUTH_TOKEN, OAuth.OAUTH_TOKEN_SECRET,
OAuth.OAUTH_CALLBACK, OAuth.OAUTH_SIGNATURE_METHOD, OAuth.OAUTH_SIGNATURE, OAuth.OAUTH_TIMESTAMP,
OAuth.OAUTH_NONCE, OAuth.OAUTH_VERSION }) {
s.add(p);
}
return Collections.unmodifiableSet(s);
}
/**
* Construct a validator that rejects messages more than five minutes old or
* with a OAuth version other than 1.0.
*/
public SimpleOAuthValidator() {
this(DEFAULT_TIMESTAMP_WINDOW, Double.parseDouble(OAuth.VERSION_1_0));
}
/**
* Public constructor.
*
* @param maxTimestampAgeMsec
* the range of valid timestamps, in milliseconds into the past
* or future. So the total range of valid timestamps is twice
* this value, rounded to the nearest second.
* @param maxVersion
* the maximum valid oauth_version
*/
public SimpleOAuthValidator(long maxTimestampAgeMsec, double maxVersion) {
this.maxTimestampAgeMsec = maxTimestampAgeMsec;
this.maxVersion = maxVersion;
}
protected final double minVersion = 1.0;
protected final double maxVersion;
protected final long maxTimestampAgeMsec;
private final Set usedNonces = new TreeSet();
/**
* Allow objects that are no longer useful to become garbage.
*
* @return the earliest point in time at which another call will release
* some garbage, or null to indicate there's nothing currently
* stored that will become garbage in future. This value may change,
* each time releaseGarbage or validateNonce is called.
*/
public Date releaseGarbage() {
return removeOldNonces(currentTimeMsec());
}
/**
* Remove usedNonces with timestamps that are too old to be valid.
*/
private Date removeOldNonces(long currentTimeMsec) {
UsedNonce next = null;
UsedNonce min = new UsedNonce((currentTimeMsec - maxTimestampAgeMsec + 500) / 1000L);
synchronized (usedNonces) {
// Because usedNonces is a TreeSet, its iterator produces
// elements from oldest to newest (their natural order).
for (Iterator iter = usedNonces.iterator(); iter.hasNext();) {
UsedNonce used = iter.next();
if (min.compareTo(used) <= 0) {
next = used;
break; // all the rest are also new enough
}
iter.remove(); // too old
}
}
if (next == null)
return null;
return new Date((next.getTimestamp() * 1000L) + maxTimestampAgeMsec + 500);
}
/** {@inherit}
* @throws URISyntaxException */
public void validateMessage(OAuthMessage message, OAuthAccessor accessor)
throws OAuthException, IOException, URISyntaxException {
checkSingleParameters(message);
validateVersion(message);
validateTimestampAndNonce(message);
validateSignature(message, accessor);
}
/** Throw an exception if any SINGLE_PARAMETERS occur repeatedly. */
protected void checkSingleParameters(OAuthMessage message) throws IOException, OAuthException {
// Check for repeated oauth_ parameters:
boolean repeated = false;
Map> nameToValues = new HashMap>();
for (Map.Entry parameter : message.getParameters()) {
String name = parameter.getKey();
if (SINGLE_PARAMETERS.contains(name)) {
Collection values = nameToValues.get(name);
if (values == null) {
values = new ArrayList();
nameToValues.put(name, values);
} else {
repeated = true;
}
values.add(parameter.getValue());
}
}
if (repeated) {
Collection rejected = new ArrayList();
for (Map.Entry> p : nameToValues.entrySet()) {
String name = p.getKey();
Collection values = p.getValue();
if (values.size() > 1) {
for (String value : values) {
rejected.add(new OAuth.Parameter(name, value));
}
}
}
OAuthProblemException problem = new OAuthProblemException(OAuth.Problems.PARAMETER_REJECTED);
problem.setParameter(OAuth.Problems.OAUTH_PARAMETERS_REJECTED, OAuth.formEncode(rejected));
throw problem;
}
}
protected void validateVersion(OAuthMessage message)
throws OAuthException, IOException {
String versionString = message.getParameter(OAuth.OAUTH_VERSION);
if (versionString != null) {
double version = Double.parseDouble(versionString);
if (version < minVersion || maxVersion < version) {
// *LAMS* added by LAMS
log.debug("Error. oauth_version parameter (" + version + ") must be greater than minVersion="
+ minVersion + " and less than maxVersion=" + maxVersion);
OAuthProblemException problem = new OAuthProblemException(OAuth.Problems.VERSION_REJECTED);
problem.setParameter(OAuth.Problems.OAUTH_ACCEPTABLE_VERSIONS, minVersion + "-" + maxVersion);
throw problem;
}
}
}
/**
* Throw an exception if the timestamp is out of range or the nonce has been
* validated previously.
*/
protected void validateTimestampAndNonce(OAuthMessage message)
throws IOException, OAuthProblemException {
message.requireParameters(OAuth.OAUTH_TIMESTAMP, OAuth.OAUTH_NONCE);
long timestamp = Long.parseLong(message.getParameter(OAuth.OAUTH_TIMESTAMP));
long now = currentTimeMsec();
validateTimestamp(message, timestamp, now);
validateNonce(message, timestamp, now);
}
/** Throw an exception if the timestamp [sec] is out of range. */
protected void validateTimestamp(OAuthMessage message, long timestamp, long currentTimeMsec) throws IOException,
OAuthProblemException {
long min = (currentTimeMsec - maxTimestampAgeMsec + 500) / 1000L;
long max = (currentTimeMsec + maxTimestampAgeMsec + 500) / 1000L;
if (timestamp < min || max < timestamp) {
// *LAMS* added by LAMS
log.debug("Error. oauth_timestamp parameter (" + timestamp + ") must be greater than min=" + min
+ " and less than max=" + max);
OAuthProblemException problem = new OAuthProblemException(OAuth.Problems.TIMESTAMP_REFUSED);
problem.setParameter(OAuth.Problems.OAUTH_ACCEPTABLE_TIMESTAMPS, min + "-" + max);
throw problem;
}
}
/**
* Throw an exception if the nonce has been validated previously.
*
* @return the earliest point in time at which a call to releaseGarbage
* will actually release some garbage, or null to indicate there's
* nothing currently stored that will become garbage in future.
*/
protected Date validateNonce(OAuthMessage message, long timestamp, long currentTimeMsec) throws IOException,
OAuthProblemException {
UsedNonce nonce = new UsedNonce(timestamp,
message.getParameter(OAuth.OAUTH_NONCE), message.getConsumerKey(), message.getToken());
/*
* The OAuth standard requires the token to be omitted from the stored
* nonce. But I include it, to harmonize with a Consumer that generates
* nonces using several independent computers, each with its own token.
*/
boolean valid = false;
synchronized (usedNonces) {
valid = usedNonces.add(nonce);
}
if (!valid) {
// *LAMS* added by LAMS
log.debug("Error. Set of usedNonces already contains newly constructed nonce (" + message.getParameter(OAuth.OAUTH_NONCE) + ")");
throw new OAuthProblemException(OAuth.Problems.NONCE_USED);
}
return removeOldNonces(currentTimeMsec);
}
protected void validateSignature(OAuthMessage message, OAuthAccessor accessor)
throws OAuthException, IOException, URISyntaxException {
message.requireParameters(OAuth.OAUTH_CONSUMER_KEY,
OAuth.OAUTH_SIGNATURE_METHOD, OAuth.OAUTH_SIGNATURE);
OAuthSignatureMethod.newSigner(message, accessor).validate(message);
}
/** Get the number of milliseconds since midnight, January 1, 1970 UTC. */
protected long currentTimeMsec() {
return System.currentTimeMillis();
}
/**
* Selected parameters from an OAuth request, in a form suitable for
* detecting duplicate requests. The implementation is optimized for the
* comparison operations (compareTo, equals and hashCode).
*
* @author John Kristian
*/
private static class UsedNonce implements Comparable {
/**
* Construct an object containing the given timestamp, nonce and other
* parameters. The order of parameters is significant.
*/
UsedNonce(long timestamp, String... nonceEtc) {
StringBuilder key = new StringBuilder(String.format("%20d", Long.valueOf(timestamp)));
// The blank padding ensures that timestamps are compared as numbers.
for (String etc : nonceEtc) {
key.append("&").append(etc == null ? " " : OAuth.percentEncode(etc));
// A null value is different from "" or any other String.
}
sortKey = key.toString();
}
private final String sortKey;
long getTimestamp() {
int end = sortKey.indexOf("&");
if (end < 0)
end = sortKey.length();
return Long.parseLong(sortKey.substring(0, end).trim());
}
/**
* Determine the relative order of this
and
* that
, as specified by Comparable. The timestamp is most
* significant; that is, if the timestamps are different, return 1 or
* -1. If this
contains only a timestamp (with no nonce
* etc.), return -1 or 0. The treatment of the nonce etc. is murky,
* although 0 is returned only if they're all equal.
*/
public int compareTo(UsedNonce that) {
return (that == null) ? 1 : sortKey.compareTo(that.sortKey);
}
@Override
public int hashCode() {
return sortKey.hashCode();
}
/**
* Return true iff this
and that
contain equal
* timestamps, nonce etc., in the same order.
*/
@Override
public boolean equals(Object that) {
if (that == null)
return false;
if (that == this)
return true;
if (that.getClass() != getClass())
return false;
return sortKey.equals(((UsedNonce) that).sortKey);
}
@Override
public String toString() {
return sortKey;
}
}
}