//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. All rights reserved. // //----------------------------------------------------------------------- namespace DotNetOpenAuth.Messaging.Bindings { using System; using System.Collections.Generic; using System.Linq; /// /// An in-memory nonce store. Useful for single-server web applications. /// NOT for web farms. /// internal class MemoryNonceStore : INonceStore { /// /// How frequently we should take time to clear out old nonces. /// private const int AutoCleaningFrequency = 10; /// /// The maximum age a message can be before it is discarded. /// /// /// This is useful for knowing how long used nonces must be retained. /// private readonly TimeSpan maximumMessageAge; /// /// A list of the consumed nonces. /// private readonly SortedDictionary> usedNonces = new SortedDictionary>(); /// /// A lock object used around accesses to the field. /// private object nonceLock = new object(); /// /// Where we're currently at in our periodic nonce cleaning cycle. /// private int nonceClearingCounter; /// /// Initializes a new instance of the class. /// internal MemoryNonceStore() : this(StandardExpirationBindingElement.MaximumMessageAge) { } /// /// Initializes a new instance of the class. /// /// The maximum age a message can be before it is discarded. internal MemoryNonceStore(TimeSpan maximumMessageAge) { this.maximumMessageAge = maximumMessageAge; } #region INonceStore Members /// /// Stores a given nonce and timestamp. /// /// The context, or namespace, within which the must be unique. /// A series of random characters. /// The timestamp that together with the nonce string make it unique. /// The timestamp may also be used by the data store to clear out old nonces. /// /// True if the nonce+timestamp (combination) was not previously in the database. /// False if the nonce was stored previously with the same timestamp. /// /// /// The nonce must be stored for no less than the maximum time window a message may /// be processed within before being discarded as an expired message. /// If the binding element is applicable to your channel, this expiration window /// is retrieved or set using the /// property. /// public bool StoreNonce(string context, string nonce, DateTime timestamp) { if (timestamp.ToUniversalTimeSafe() + this.maximumMessageAge < DateTime.UtcNow) { // The expiration binding element should have taken care of this, but perhaps // it's at the boundary case. We should fail just to be safe. return false; } // We just concatenate the context with the nonce to form a complete, namespace-protected nonce. string completeNonce = context + "\0" + nonce; lock (this.nonceLock) { List nonces; if (!this.usedNonces.TryGetValue(timestamp, out nonces)) { this.usedNonces[timestamp] = nonces = new List(4); } if (nonces.Contains(completeNonce)) { return false; } nonces.Add(completeNonce); // Clear expired nonces if it's time to take a moment to do that. // Unchecked so that this can int overflow without an exception. unchecked { this.nonceClearingCounter++; } if (this.nonceClearingCounter % AutoCleaningFrequency == 0) { this.ClearExpiredNonces(); } return true; } } #endregion /// /// Clears consumed nonces from the cache that are so old they would be /// rejected if replayed because it is expired. /// public void ClearExpiredNonces() { lock (this.nonceLock) { var oldNonceLists = this.usedNonces.Keys.Where(time => time.ToUniversalTimeSafe() + this.maximumMessageAge < DateTime.UtcNow).ToList(); foreach (DateTime time in oldNonceLists) { this.usedNonces.Remove(time); } // Reset the auto-clean counter so that if this method was called externally // we don't auto-clean right away. this.nonceClearingCounter = 0; } } } }