//-----------------------------------------------------------------------
//
// Copyright (c) Andrew Arnott. All rights reserved.
//
//-----------------------------------------------------------------------
namespace RelyingPartyLogic {
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Linq;
using System.Text;
using DotNetOpenAuth.Configuration;
using DotNetOpenAuth.Messaging.Bindings;
///
/// A database-backed nonce store for OpenID and OAuth services.
///
public class NonceDbStore : INonceStore {
private const int NonceClearingInterval = 5;
///
/// A counter that tracks how many nonce stores have been done.
///
private static int nonceClearingCounter;
///
/// Initializes a new instance of the class.
///
public NonceDbStore() {
}
#region INonceStore Members
///
/// Stores a given nonce and timestamp.
///
/// The context, or namespace, within which the
/// must be unique.
/// The context SHOULD be treated as case-sensitive.
/// The value will never be null but may be the empty string.
/// A series of random characters.
/// The UTC timestamp that together with the nonce string make it unique
/// within the given .
/// The timestamp may also be used by the data store to clear out old nonces.
///
/// True if the context+nonce+timestamp (combination) was not previously in the database.
/// False if the nonce was stored previously with the same timestamp and context.
///
///
/// 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.
/// This maximum message age can be looked up via the
///
/// property, accessible via the
/// property.
///
public bool StoreNonce(string context, string nonce, DateTime timestampUtc) {
try {
using (var dataContext = new TransactedDatabaseEntities(IsolationLevel.ReadCommitted)) {
Nonce nonceEntity = new Nonce {
Context = context,
Code = nonce,
IssuedUtc = timestampUtc,
ExpiresUtc = timestampUtc + DotNetOpenAuthSection.Configuration.Messaging.MaximumMessageLifetime,
};
// The database columns [context] and [code] MUST be using
// a case sensitive collation for this to be secure.
dataContext.AddToNonces(nonceEntity);
}
} catch (UpdateException) {
// A nonce collision
return false;
}
// Only clear nonces after successfully storing a nonce.
// This mitigates cheap DoS attacks that take up a lot of
// database cycles.
ClearNoncesIfAppropriate();
return true;
}
#endregion
///
/// Clears the nonces if appropriate.
///
private static void ClearNoncesIfAppropriate() {
if (++nonceClearingCounter % NonceClearingInterval == 0) {
using (var dataContext = new TransactedDatabaseEntities(IsolationLevel.ReadCommitted)) {
dataContext.ClearExpiredNonces();
}
}
}
///
/// A transacted data context.
///
protected class TransactedDatabaseEntities : DatabaseEntities {
///
/// The transaction for this data context.
///
private DbTransaction transaction;
///
/// Initializes a new instance of the class.
///
/// The isolation level.
public TransactedDatabaseEntities(IsolationLevel isolationLevel) {
this.Connection.Open();
this.transaction = this.Connection.BeginTransaction(isolationLevel);
}
///
/// Releases the resources used by the object context.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected override void Dispose(bool disposing) {
try {
this.SaveChanges();
this.transaction.Commit();
} finally {
this.Connection.Close();
}
base.Dispose(disposing);
}
}
}
}