/* * Copyright 2008 Web Cohesion * * 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 org.springframework.security.oauth.provider.nonce; import java.util.Iterator; import java.util.TreeSet; import org.springframework.security.authentication.CredentialsExpiredException; import org.springframework.security.oauth.provider.ConsumerDetails; /** * Expands on the {@link org.springframework.security.oauth.provider.nonce.ExpiringTimestampNonceServices} to include * validation of the nonce for replay protection. * * To validate the nonce, the InMemoryNonceService first validates the consumer key and timestamp as does the * {@link org.springframework.security.oauth.provider.nonce.ExpiringTimestampNonceServices}. Assuming the consumer and * timestamp are valid, the InMemoryNonceServices further ensures that the specified nonce was not used with the * specified timestamp within the specified validity window. The list of nonces used within the validity window is kept * in memory. * * Note: the default validity window in this class is different from the one used in * {@link org.springframework.security.oauth.provider.nonce.ExpiringTimestampNonceServices}. The reason for this is that * this class has a per request memory overhead. Keeping the validity window short helps prevent wasting a lot of * memory. 10 minutes that allows for minor variations in time between servers. * * @author Ryan Heaton * @author Jilles van Gurp */ public class InMemoryNonceServices implements OAuthNonceServices { /** * Contains all the nonces that were used inside the validity window. */ static final TreeSet<NonceEntry> NONCES = new TreeSet<NonceEntry>(); private volatile long lastCleaned = 0; // we'll default to a 10 minute validity window, otherwise the amount of memory used on NONCES can get quite large. private long validityWindowSeconds = 60 * 10; public void validateNonce(ConsumerDetails consumerDetails, long timestamp, String nonce) { if (System.currentTimeMillis() / 1000 - timestamp > getValidityWindowSeconds()) { throw new CredentialsExpiredException("Expired timestamp."); } NonceEntry entry = new NonceEntry(consumerDetails.getConsumerKey(), timestamp, nonce); synchronized (NONCES) { if (NONCES.contains(entry)) { throw new NonceAlreadyUsedException("Nonce already used: " + nonce); } else { NONCES.add(entry); } cleanupNonces(); } } private void cleanupNonces() { long now = System.currentTimeMillis() / 1000; // don't clean out the NONCES for each request, this would cause the service to be constantly locked on this // loop under load. One second is small enough that cleaning up does not become too expensive. // Also see SECOAUTH-180 for reasons this class was refactored. if (now - lastCleaned > 1) { Iterator<NonceEntry> iterator = NONCES.iterator(); while (iterator.hasNext()) { // the nonces are already sorted, so simply iterate and remove until the first nonce within the validity // window. NonceEntry nextNonce = iterator.next(); long difference = now - nextNonce.timestamp; if (difference > getValidityWindowSeconds()) { iterator.remove(); } else { break; } } // keep track of when cleanupNonces last ran lastCleaned = now; } } /** * Set the timestamp validity window (in seconds). * * @return the timestamp validity window (in seconds). */ public long getValidityWindowSeconds() { return validityWindowSeconds; } /** * The timestamp validity window (in seconds). * * @param validityWindowSeconds the timestamp validity window (in seconds). */ public void setValidityWindowSeconds(long validityWindowSeconds) { this.validityWindowSeconds = validityWindowSeconds; } /** * Representation of a nonce with the right hashCode, equals, and compareTo methods for the TreeSet approach above * to work. */ static class NonceEntry implements Comparable<NonceEntry> { private final String consumerKey; private final long timestamp; private final String nonce; public NonceEntry(String consumerKey, long timestamp, String nonce) { this.consumerKey = consumerKey; this.timestamp = timestamp; this.nonce = nonce; } @Override public int hashCode() { return consumerKey.hashCode() * nonce.hashCode() * Long.valueOf(timestamp).hashCode(); } @Override public boolean equals(Object obj) { if (obj == null || !(obj instanceof NonceEntry)) { return false; } NonceEntry arg = (NonceEntry) obj; return timestamp == arg.timestamp && consumerKey.equals(arg.consumerKey) && nonce.equals(arg.nonce); } public int compareTo(NonceEntry o) { // sort by timestamp if (timestamp < o.timestamp) { return -1; } else if (timestamp == o.timestamp) { int consumerKeyCompare = consumerKey.compareTo(o.consumerKey); if (consumerKeyCompare == 0) { return nonce.compareTo(o.nonce); } else { return consumerKeyCompare; } } else { return 1; } } @Override public String toString() { return timestamp + " " + consumerKey + " " + nonce; } } }