using System; using System.Collections.Specialized; using System.ComponentModel; using System.Web; using System.Web.UI; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; using System.Web.Security; namespace CKS.FormsBasedAuthentication.HIP { /// The base class for all Reverse Turing Test challenges. public abstract class HipChallenge : WebControl, INamingContainer { /// Default value for AlwaysGenerateNewChallenge. private const bool ALWAYS_GEN_DEFAULT = true; /// Default value for Expiration. private const int EXPIRATION_DEFAULT = 120; /// Stores the challenge ID associated for future requests. private HtmlInputHidden _hiddenData; /// The list of words that can be rendered. private StringCollection _words; /// Whether Authenticate has previously succeeded in this HttpRequest. private bool _authenticated; /// Backing store for Expiration. private int _expiration = EXPIRATION_DEFAULT; /// Random number generator used to create the HIP. private RandomNumbers _rand = new RandomNumbers(); /// Gets a random non-negative integer less than max. /// The upper-bound for the random number. /// The generated random number. protected int NextRandom(int max) { return _rand.Next(max); } /// Gets a random number between min and max, inclusive. /// The minimum possible value. /// The maximum possible value. /// The randomly generated number. protected int NextRandom(int min, int max) { return _rand.Next(min, max); } /// Gets a randomly generated double between 0.0 and 1.1. /// The random number. protected double NextRandomDouble() { return _rand.NextDouble(); } /// Gets or sets the duration of time (seconds) a user has before the challenge expires. /// The duration of time (seconds) a user has before the challenge expires. [Category("Behavior")] [Description("The duration of time (seconds) a user has before the challenge expires.")] [DefaultValue(EXPIRATION_DEFAULT)] public int Expiration { get { return _expiration; } set { _expiration = value; } } /// Gets the list of words the control can use to create challenges. /// The list of words the control can use to create images. [EditorBrowsable(EditorBrowsableState.Never)] public StringCollection Words { get { if (_words == null) _words = new StringCollection(); return _words; } set { _words = value; } } /// Selects a word to use in an image. /// The word to use in the challenge. protected virtual string ChooseWord() { if (Words.Count == 0) throw new InvalidOperationException("No words available for challenge."); return Words[NextRandom(Words.Count)]; } /// Creates the hidden field control that stores the challenge ID. protected override void CreateChildControls() { _hiddenData = new HtmlInputHidden(); _hiddenData.EnableViewState = false; Controls.Add(_hiddenData); base.CreateChildControls(); } /// Generates a new image and fills in the dynamic image and hidden field appropriately. /// Ignored. protected sealed override void OnPreRender(EventArgs e) { // Gets a word for the challenge, associates it with a new ID, and stores it for the client string content = ChooseWord(); Guid id = Guid.NewGuid(); SetChallengeText(id, content, DateTime.Now.AddSeconds(Expiration)); _hiddenData.Value = id.ToString("N"); // Generates a challenge based on the selected word/phrase. RenderChallenge(id, content); base.OnPreRender(e); } /// Gets the challenge text for a particular ID. /// The ID of the challenge text to retrieve. /// The text associated with the specified ID; null if no text exists. internal static string GetChallengeText(Guid challengeId) { HttpContext ctx = HttpContext.Current; return (string)ctx.Cache[challengeId.ToString()]; } /// Sets the challenge text for a particular ID. /// The ID of the challenge with which this text should be associated. /// The text to store along with the challenge ID. /// The expiration date fo the challenge. internal static void SetChallengeText(Guid challengeId, string text, DateTime expiration) { HttpContext ctx = HttpContext.Current; if (text == null) ctx.Cache.Remove(challengeId.ToString()); else ctx.Cache.Insert(challengeId.ToString(), text, null, expiration, System.Web.Caching.Cache.NoSlidingExpiration); } /// Authenticates user-supplied data against that retrieved using the challenge ID. /// The user-supplied data. /// Whether the user-supplied data matches that retrieved using the challenge ID. internal bool Authenticate(string userData) { // We want to allow multiple authentication requests within the same HTTP request, // so we can the result as a member variable of the class (non-static) if (_authenticated == true) return _authenticated; // If no authentication has happened previously, and if the user has supplied text, // and if the ID is stored correctly in the page, and if the user text matches the challenge text, // then set the challenge text, note that we've authenticated, and return true. Otherwise, failed authentication. if (userData != null && userData.Length > 0 && _hiddenData != null && _hiddenData.Value != null && _hiddenData.Value.Length > 0) { try { Guid id = new Guid(_hiddenData.Value); string text = GetChallengeText(id); if (text != null && string.Compare(userData, text, true) == 0) { _authenticated = true; SetChallengeText(id, null, DateTime.MinValue); return true; } } catch(FormatException){} } return false; } /// Generats the challenge and presents it to the user. /// The ID of the challenge. /// The content to render. protected abstract void RenderChallenge(Guid id, string content); } }