Created
September 21, 2013 18:30
-
-
Save miketrebilcock/6652969 to your computer and use it in GitHub Desktop.
CustomApplicationStore for use with Servicestack when running self-hosted. Need to include DotNetOpenAuth in project.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using DotNetOpenAuth.Configuration; | |
using DotNetOpenAuth.Messaging.Bindings; | |
using DotNetOpenAuth.OpenId; | |
using ServiceStack.Logging; | |
using ServiceStack.Redis; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
namespace CORE.STS | |
{ | |
/// <summary> | |
/// Heavily inspired by | |
/// https://knowledgeexchange.svn.codeplex.com/svn/binaries/DotNetOpenAuth-3.2.2.9257/Samples/OpenIdProviderWebForms/Code/CustomStore.cs | |
/// by Andrew Arnott | |
/// </summary> | |
public class CustomApplicationStore : IOpenIdApplicationStore | |
{ | |
/// <summary> | |
/// How frequently to check for and remove expired secrets. | |
/// </summary> | |
private static readonly TimeSpan cleaningInterval = TimeSpan.FromMinutes(30); | |
private readonly string CacheKey = "STS-KeyBucket-"; | |
/// <summary> | |
/// The last time the cache had expired keys removed from it. | |
/// </summary> | |
private DateTime lastCleaning = DateTime.UtcNow; | |
private ILog log = LogManager.GetLogger(typeof(CustomApplicationStore)); | |
public CustomApplicationStore() | |
{ | |
} | |
/// <summary> | |
/// Gets the key in a given bucket and handle. | |
/// </summary> | |
/// <param name="bucket">The bucket name. Case sensitive.</param> | |
/// <param name="handle">The key handle. Case sensitive.</param> | |
/// <returns> | |
/// The cryptographic key, or <c>null</c> if no matching key was found. | |
/// </returns> | |
public CryptoKey GetKey(string bucket, string handle) | |
{ | |
log.Debug("Request for Key from " + bucket + " with handle " + handle); | |
Dictionary<string, plainKey> store = getStore(bucket); | |
this.CleanExpiredKeysFromCacheIfAppropriate(ref store); | |
if (store != null) | |
{ | |
plainKey key; | |
if (store.TryGetValue(handle, out key)) | |
{ | |
log.Debug("Key Found"); | |
return new CryptoKey(key.key, key.ExpiresUtc); | |
} | |
} | |
log.Debug("Key Not Found"); | |
return null; | |
} | |
/// <summary> | |
/// Gets a sequence of existing keys within a given bucket. | |
/// </summary> | |
/// <param name="bucket">The bucket name. Case sensitive.</param> | |
/// <returns> | |
/// A sequence of handles and keys, ordered by descending <see cref="CryptoKey.ExpiresUtc"/>. | |
/// </returns> | |
public IEnumerable<KeyValuePair<string, CryptoKey>> GetKeys(string bucket) | |
{ | |
log.Debug("Request for All Keys from " + bucket); | |
Dictionary<string, plainKey> store = getStore(bucket); | |
this.CleanExpiredKeysFromCacheIfAppropriate(ref store); | |
if (store != null) | |
{ | |
log.Debug("Bucket found with " + store.Count + " Keys"); | |
List<KeyValuePair<string, CryptoKey>> keys = new List<KeyValuePair<string, CryptoKey>>(); | |
foreach (KeyValuePair<string, plainKey> key in store.ToList()) | |
{ | |
keys.Add(new KeyValuePair<string, CryptoKey>(key.Key, new CryptoKey(key.Value.key, key.Value.ExpiresUtc))); | |
} | |
return keys; | |
} | |
else | |
{ | |
log.Debug("Bucket not found " + bucket); | |
return Enumerable.Empty<KeyValuePair<string, CryptoKey>>(); | |
} | |
} | |
/// <summary> | |
/// Removes the key. | |
/// </summary> | |
/// <param name="bucket">The bucket name. Case sensitive.</param> | |
/// <param name="handle">The key handle. Case sensitive.</param> | |
public void RemoveKey(string bucket, string handle) | |
{ | |
log.Debug("Remove Key from " + bucket + " with handle " + handle); | |
Dictionary<string, plainKey> store = getStore(bucket); | |
store.Remove(handle); | |
this.CleanExpiredKeysFromCacheIfAppropriate(ref store); | |
saveStore(bucket, store); | |
} | |
/// <summary> | |
/// Stores a cryptographic key. | |
/// </summary> | |
/// <param name="bucket">The name of the bucket to store the key in. Case sensitive.</param> | |
/// <param name="handle">The handle to the key, unique within the bucket. Case sensitive.</param> | |
/// <param name="key">The key to store.</param> | |
/// <exception cref="CryptoKeyCollisionException">Thrown in the event of a conflict with an existing key in the same bucket and with the same handle.</exception> | |
public void StoreKey(string bucket, string handle, CryptoKey key) | |
{ | |
log.Debug("Store Key in " + bucket + " with handle " + handle); | |
Dictionary<string, plainKey> store = getStore(bucket); | |
if (store == null) | |
{ | |
store = new Dictionary<string, plainKey>(StringComparer.Ordinal); | |
} | |
if (store.ContainsKey(handle)) | |
{ | |
throw new CryptoKeyCollisionException(); | |
} | |
plainKey saveKey = new plainKey() { key = key.Key, ExpiresUtc = key.ExpiresUtc }; | |
store[handle] = saveKey; | |
this.CleanExpiredKeysFromCacheIfAppropriate(ref store); | |
if (store.Count == 0) | |
{ | |
log.Debug("Bucket " + bucket + " is empty"); | |
using (var cache = new RedisNativeClient(AppConfig.getAppConfig().REDISUrlForNativeClient, AppConfig.getAppConfig().REDISPort, AppConfig.getAppConfig().REDISPasswordForNativeClient)) | |
{ | |
cache.Del(CacheKey + bucket); | |
} | |
} | |
else | |
{ | |
log.Debug("New Bucket " + bucket + " Added"); | |
saveStore(bucket, store); | |
} | |
} | |
/// <summary> | |
/// Stores a given nonce and timestamp. | |
/// </summary> | |
/// <param name="context">The context, or namespace, within which the | |
/// <paramref name="nonce"/> must be unique. | |
/// The context SHOULD be treated as case-sensitive. | |
/// The value will never be <c>null</c> but may be the empty string.</param> | |
/// <param name="nonce">A series of random characters.</param> | |
/// <param name="timestampUtc">The UTC timestamp that together with the nonce string make it unique | |
/// within the given <paramref name="context"/>. | |
/// The timestamp may also be used by the data store to clear out old nonces.</param> | |
/// <returns> | |
/// 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. | |
/// </returns> | |
/// <remarks> | |
/// 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 | |
/// <see cref="DotNetOpenAuth.Configuration.MessagingElement.MaximumMessageLifetime"/> | |
/// property, accessible via the <see cref="DotNetOpenAuth.Configuration.MessagingElement.Configuration"/> | |
/// property. | |
/// </remarks> | |
public bool StoreNonce(string context, string nonce, DateTime timestampUtc) | |
{ | |
DateTime ExpiresUtc = timestampUtc + DotNetOpenAuthSection.Messaging.MaximumMessageLifetime; | |
int seconds = (int)(ExpiresUtc - DateTime.UtcNow).TotalSeconds; | |
string key = "key-nonce-" + context + "-" + nonce; | |
using (var cache = new RedisNativeClient(AppConfig.getAppConfig().REDISUrlForNativeClient, AppConfig.getAppConfig().REDISPort, AppConfig.getAppConfig().REDISPasswordForNativeClient)) | |
{ | |
if (cache.Get(key) != null) return false; | |
byte[] data = { 1 }; | |
cache.SetEx(key, seconds, data); | |
} | |
return true; | |
} | |
/// <summary> | |
/// Cleans the expired keys from cache if the cleaning interval has passed. | |
/// </summary> | |
private void CleanExpiredKeysFromCacheIfAppropriate(ref Dictionary<string, plainKey> store) | |
{ | |
if (DateTime.UtcNow > this.lastCleaning + cleaningInterval) | |
{ | |
this.ClearExpiredKeysFromStore(ref store); | |
} | |
} | |
/// <summary> | |
/// Weeds out expired keys from the in-memory cache. | |
/// </summary> | |
private void ClearExpiredKeysFromStore(ref Dictionary<string, plainKey> store) | |
{ | |
log.Debug("Clearing Expired Keys"); | |
var expiredKeys = new List<string>(); | |
foreach (var handlePair in store) | |
{ | |
if (handlePair.Value.ExpiresUtc < DateTime.UtcNow) | |
{ | |
expiredKeys.Add(handlePair.Key); | |
} | |
} | |
foreach (var expiredKey in expiredKeys) | |
{ | |
store.Remove(expiredKey); | |
} | |
this.lastCleaning = DateTime.UtcNow; | |
} | |
private Dictionary<string, plainKey> getStore(string bucket) | |
{ | |
using (var cache = new RedisNativeClient(AppConfig.getAppConfig().REDISUrlForNativeClient, AppConfig.getAppConfig().REDISPort, AppConfig.getAppConfig().REDISPasswordForNativeClient)) | |
{ | |
return (Dictionary<string, plainKey>)Util.ByteArrayToObject(cache.Get(CacheKey + bucket)); | |
} | |
} | |
private void saveStore(string bucket, Dictionary<string, plainKey> store) | |
{ | |
using (var cache = new RedisNativeClient(AppConfig.getAppConfig().REDISUrlForNativeClient, AppConfig.getAppConfig().REDISPort, AppConfig.getAppConfig().REDISPasswordForNativeClient)) | |
{ | |
cache.Set(CacheKey + bucket, Util.ObjectToByteArray(store)); | |
} | |
} | |
[Serializable] | |
private class plainKey | |
{ | |
public DateTime ExpiresUtc { get; set; } | |
public byte[] key { get; set; } | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment