//-----------------------------------------------------------------------
//
// 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;
}
}
}
}