using System; using System.Data; using System.Globalization; using System.Security.Cryptography; using DotNetOpenId; using DotNetOpenId.RelyingParty; namespace RelyingPartyCustomStore { /// /// This custom store serializes all elements to demonstrate peristent and/or shared storage. /// This is common in a web farm, for example. /// /// /// 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. /// public class CustomStore : IRelyingPartyApplicationStore { public static CustomStore Instance = new CustomStore(); public CustomStoreDataSet dataSet = new CustomStoreDataSet(); #region IAssociationStore 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() { removeExpiredRows(dataSet.Association, dataSet.Association.ExpiresColumn.ColumnName); } #endregion #region INonceStore Members byte[] secretSigningKey; public byte[] SecretSigningKey { get { if (secretSigningKey == null) { lock (this) { if (secretSigningKey == null) { // initialize in a local variable before setting in field for thread safety. byte[] auth_key = new byte[64]; new RNGCryptoServiceProvider().GetBytes(auth_key); this.secretSigningKey = auth_key; } } } return secretSigningKey; } } public bool TryStoreNonce(Nonce nonce) { // 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.Code) != null) return false; dataSet.Nonce.AddNonceRow(nonce.Code, nonce.ExpirationDate.ToLocalTime()); return true; } } public void ClearExpiredNonces() { removeExpiredRows(dataSet.Nonce, dataSet.Nonce.ExpiresColumn.ColumnName); } #endregion 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); } } }