Skip to content

Instantly share code, notes, and snippets.

@miketrebilcock
Created September 21, 2013 18:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save miketrebilcock/6652969 to your computer and use it in GitHub Desktop.
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.
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