View Javadoc
1   /*
2    * Copyright 2008 Web Cohesion
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.security.oauth.provider.nonce;
18  
19  import java.util.Iterator;
20  import java.util.TreeSet;
21  
22  import org.springframework.security.authentication.CredentialsExpiredException;
23  import org.springframework.security.oauth.provider.ConsumerDetails;
24  
25  /**
26   * Expands on the {@link org.springframework.security.oauth.provider.nonce.ExpiringTimestampNonceServices} to include
27   * validation of the nonce for replay protection.
28   * 
29   * To validate the nonce, the InMemoryNonceService first validates the consumer key and timestamp as does the
30   * {@link org.springframework.security.oauth.provider.nonce.ExpiringTimestampNonceServices}. Assuming the consumer and
31   * timestamp are valid, the InMemoryNonceServices further ensures that the specified nonce was not used with the
32   * specified timestamp within the specified validity window. The list of nonces used within the validity window is kept
33   * in memory.
34   *
35   * Note: the default validity window in this class is different from the one used in
36   * {@link org.springframework.security.oauth.provider.nonce.ExpiringTimestampNonceServices}. The reason for this is that
37   * this class has a per request memory overhead. Keeping the validity window short helps prevent wasting a lot of
38   * memory. 10 minutes that allows for minor variations in time between servers.
39   *
40   * @author Ryan Heaton
41   * @author Jilles van Gurp
42   */
43  public class InMemoryNonceServices implements OAuthNonceServices {
44  
45  	/**
46  	 * Contains all the nonces that were used inside the validity window.
47  	 */
48  	static final TreeSet<NonceEntry> NONCES = new TreeSet<NonceEntry>();
49  
50  	private volatile long lastCleaned = 0;
51  
52  	// we'll default to a 10 minute validity window, otherwise the amount of memory used on NONCES can get quite large.
53  	private long validityWindowSeconds = 60 * 10;
54  
55  	public void validateNonce(ConsumerDetails consumerDetails, long timestamp, String nonce) {
56  		if (System.currentTimeMillis() / 1000 - timestamp > getValidityWindowSeconds()) {
57  			throw new CredentialsExpiredException("Expired timestamp.");
58  		}
59  
60  		NonceEntry entry = new NonceEntry(consumerDetails.getConsumerKey(), timestamp, nonce);
61  
62  		synchronized (NONCES) {
63  			if (NONCES.contains(entry)) {
64  				throw new NonceAlreadyUsedException("Nonce already used: " + nonce);
65  			}
66  			else {
67  				NONCES.add(entry);
68  			}
69  			cleanupNonces();
70  		}
71  	}
72  
73  	private void cleanupNonces() {
74  		long now = System.currentTimeMillis() / 1000;
75  		// don't clean out the NONCES for each request, this would cause the service to be constantly locked on this
76  		// loop under load. One second is small enough that cleaning up does not become too expensive.
77  		// Also see SECOAUTH-180 for reasons this class was refactored.
78  		if (now - lastCleaned > 1) {
79  			Iterator<NonceEntry> iterator = NONCES.iterator();
80  			while (iterator.hasNext()) {
81  				// the nonces are already sorted, so simply iterate and remove until the first nonce within the validity
82  				// window.
83  				NonceEntry nextNonce = iterator.next();
84  				long difference = now - nextNonce.timestamp;
85  				if (difference > getValidityWindowSeconds()) {
86  					iterator.remove();
87  				}
88  				else {
89  					break;
90  				}
91  			}
92  			// keep track of when cleanupNonces last ran
93  			lastCleaned = now;
94  		}
95  	}
96  
97  	/**
98  	 * Set the timestamp validity window (in seconds).
99  	 *
100 	 * @return the timestamp validity window (in seconds).
101 	 */
102 	public long getValidityWindowSeconds() {
103 		return validityWindowSeconds;
104 	}
105 
106 	/**
107 	 * The timestamp validity window (in seconds).
108 	 *
109 	 * @param validityWindowSeconds the timestamp validity window (in seconds).
110 	 */
111 	public void setValidityWindowSeconds(long validityWindowSeconds) {
112 		this.validityWindowSeconds = validityWindowSeconds;
113 	}
114 
115 	/**
116 	 * Representation of a nonce with the right hashCode, equals, and compareTo methods for the TreeSet approach above
117 	 * to work.
118 	 */
119 	static class NonceEntry implements Comparable<NonceEntry> {
120 		private final String consumerKey;
121 
122 		private final long timestamp;
123 
124 		private final String nonce;
125 
126 		public NonceEntry(String consumerKey, long timestamp, String nonce) {
127 			this.consumerKey = consumerKey;
128 			this.timestamp = timestamp;
129 			this.nonce = nonce;
130 		}
131 
132 		@Override
133 		public int hashCode() {
134 			return consumerKey.hashCode() * nonce.hashCode() * Long.valueOf(timestamp).hashCode();
135 		}
136 
137 		@Override
138 		public boolean equals(Object obj) {
139 			if (obj == null || !(obj instanceof NonceEntry)) {
140 				return false;
141 			}
142 			NonceEntry arg = (NonceEntry) obj;
143 			return timestamp == arg.timestamp && consumerKey.equals(arg.consumerKey) && nonce.equals(arg.nonce);
144 		}
145 
146 		public int compareTo(NonceEntry o) {
147 			// sort by timestamp
148 			if (timestamp < o.timestamp) {
149 				return -1;
150 			}
151 			else if (timestamp == o.timestamp) {
152 				int consumerKeyCompare = consumerKey.compareTo(o.consumerKey);
153 				if (consumerKeyCompare == 0) {
154 					return nonce.compareTo(o.nonce);
155 				}
156 				else {
157 					return consumerKeyCompare;
158 				}
159 			}
160 			else {
161 				return 1;
162 			}
163 		}
164 
165 		@Override
166 		public String toString() {
167 			return timestamp + " " + consumerKey + " " + nonce;
168 		}
169 	}
170 }