diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2009-01-31 21:32:01 -0800 |
---|---|---|
committer | Andrew <andrewarnott@gmail.com> | 2009-01-31 21:32:01 -0800 |
commit | e865ec7da293496d6c2e67039b86c4ef9ded5a6c (patch) | |
tree | 116d6a4e8c0892d2be13acd31bacae004de4bb8b /samples/RelyingPartyWebForms/Code/CustomStore.cs | |
parent | f6d5493a42d07389421763c2f1da35b4ca7d9f45 (diff) | |
download | DotNetOpenAuth-e865ec7da293496d6c2e67039b86c4ef9ded5a6c.zip DotNetOpenAuth-e865ec7da293496d6c2e67039b86c4ef9ded5a6c.tar.gz DotNetOpenAuth-e865ec7da293496d6c2e67039b86c4ef9ded5a6c.tar.bz2 |
Added OpenID sample sites: RP MVC, RP WebForms, RP Classic ASP, OP WebForms.
Diffstat (limited to 'samples/RelyingPartyWebForms/Code/CustomStore.cs')
-rw-r--r-- | samples/RelyingPartyWebForms/Code/CustomStore.cs | 137 |
1 files changed, 137 insertions, 0 deletions
diff --git a/samples/RelyingPartyWebForms/Code/CustomStore.cs b/samples/RelyingPartyWebForms/Code/CustomStore.cs new file mode 100644 index 0000000..b250919 --- /dev/null +++ b/samples/RelyingPartyWebForms/Code/CustomStore.cs @@ -0,0 +1,137 @@ +namespace RelyingPartyWebForms.Code { + using System; + using System.Data; + using System.Globalization; + using System.Security.Cryptography; + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// This custom store serializes all elements to demonstrate peristent and/or shared storage. + /// This is common in a web farm, for example. + /// </summary> + /// <remarks> + /// This doesn't actually serialize anything to a persistent store, so restarting the web server + /// will still clear everything this store is supposed to remember. + /// But we "persist" all associations and nonces into a DataTable to demonstrate + /// that using a database is possible. + /// </remarks> + public class CustomStore : IRelyingPartyApplicationStore { + private static CustomStoreDataSet dataSet = new CustomStoreDataSet(); + + #region IPrivateSecretStore Members + + /// <summary> + /// Gets or sets a secret key that can be used for signing. + /// </summary> + /// <value>A 64-byte binary value, which may contain null bytes.</value> + public byte[] PrivateSecret { get; set; } + + #endregion + + #region INonceStore Members + + /// <summary> + /// Stores a given nonce and timestamp. + /// </summary> + /// <param name="nonce">A series of random characters.</param> + /// <param name="timestamp">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.</param> + /// <returns> + /// True if the nonce+timestamp (combination) was not previously in the database. + /// False if the nonce was stored previously with the same timestamp. + /// </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. + /// If the binding element is applicable to your channel, this expiration window + /// is retrieved or set using the + /// <see cref="StandardExpirationBindingElement.MaximumMessageAge"/> property. + /// </remarks> + public bool StoreNonce(string nonce, DateTime timestamp) { + // IMPORTANT: If actually persisting to a database that can be reached from + // different servers/instances of this class at once, it is vitally important + // to protect against race condition attacks by one or more of these: + // 1) setting a UNIQUE constraint on the nonce CODE in the SQL table + // 2) Using a transaction with repeatable reads to guarantee that a check + // that verified a nonce did not exist will prevent that nonce from being + // added by another process while this process is adding it. + // And then you'll want to catch the exception that the SQL database can throw + // at you in the result of a race condition somewhere in your web site UI code + // and display some message to have the user try to log in again, and possibly + // warn them about a replay attack. + lock (this) { + if (dataSet.Nonce.FindByCode(nonce) != null) { + return false; + } + + // TODO: calculate the actual expiration date. + dataSet.Nonce.AddNonceRow(nonce, timestamp.ToLocalTime(), (timestamp + TimeSpan.FromHours(1)).ToLocalTime()); + return true; + } + } + + public void ClearExpiredNonces() { + this.removeExpiredRows(dataSet.Nonce, dataSet.Nonce.ExpiresColumn.ColumnName); + } + + #endregion + + #region IAssociationStore<Uri> Members + + public void StoreAssociation(Uri distinguishingFactor, Association assoc) { + var assocRow = dataSet.Association.NewAssociationRow(); + assocRow.DistinguishingFactor = distinguishingFactor.AbsoluteUri; + assocRow.Handle = assoc.Handle; + assocRow.Expires = assoc.Expires.ToLocalTime(); + assocRow.PrivateData = assoc.SerializePrivateData(); + dataSet.Association.AddAssociationRow(assocRow); + } + + public Association GetAssociation(Uri distinguishingFactor) { + // properly escape the URL to prevent injection attacks. + string value = distinguishingFactor.AbsoluteUri.Replace("'", "''"); + string filter = string.Format( + CultureInfo.InvariantCulture, + "{0} = '{1}'", + dataSet.Association.DistinguishingFactorColumn.ColumnName, + value); + string sort = dataSet.Association.ExpiresColumn.ColumnName + " DESC"; + DataView view = new DataView(dataSet.Association, filter, sort, DataViewRowState.CurrentRows); + if (view.Count == 0) { + return null; + } + var row = (CustomStoreDataSet.AssociationRow)view[0].Row; + return Association.Deserialize(row.Handle, row.Expires.ToUniversalTime(), row.PrivateData); + } + + public Association GetAssociation(Uri distinguishingFactor, string handle) { + var assocRow = dataSet.Association.FindByDistinguishingFactorHandle(distinguishingFactor.AbsoluteUri, handle); + return Association.Deserialize(assocRow.Handle, assocRow.Expires, assocRow.PrivateData); + } + + public bool RemoveAssociation(Uri distinguishingFactor, string handle) { + var row = dataSet.Association.FindByDistinguishingFactorHandle(distinguishingFactor.AbsoluteUri, handle); + if (row != null) { + dataSet.Association.RemoveAssociationRow(row); + return true; + } else { + return false; + } + } + + public void ClearExpiredAssociations() { + this.removeExpiredRows(dataSet.Association, dataSet.Association.ExpiresColumn.ColumnName); + } + + #endregion + + private void removeExpiredRows(DataTable table, string expiredColumnName) { + string filter = string.Format(CultureInfo.InvariantCulture, "{0} < #{1}#", expiredColumnName, DateTime.Now); + DataView view = new DataView(table, filter, null, DataViewRowState.CurrentRows); + for (int i = view.Count - 1; i >= 0; i--) { + view.Delete(i); + } + } + } +} |