summaryrefslogtreecommitdiffstats
path: root/samples/OpenIdProviderWebForms/Code/CustomStore.cs
blob: 34a3bc45504cde9f9160a0cc6f896370a3bdfdb1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
//-----------------------------------------------------------------------
// <copyright file="CustomStore.cs" company="Andrew Arnott">
//     Copyright (c) Andrew Arnott. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace OpenIdProviderWebForms.Code {
	using System;
	using System.Data;
	using System.Globalization;
	using System.Security.Cryptography;
	using DotNetOpenAuth.OpenId;
	using IProviderAssociationStore = DotNetOpenAuth.OpenId.IAssociationStore<DotNetOpenAuth.OpenId.AssociationRelyingPartyType>;
	using DotNetOpenAuth.OpenId.Provider;

	/// <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 : IProviderApplicationStore {
		private static CustomStoreDataSet dataSet = new CustomStoreDataSet();

		#region IAssociationStore<AssociationRelyingPartyType> Members

		public void StoreAssociation(AssociationRelyingPartyType distinguishingFactor, Association assoc) {
			var assocRow = dataSet.Association.NewAssociationRow();
			assocRow.DistinguishingFactor = distinguishingFactor.ToString();
			assocRow.Handle = assoc.Handle;
			assocRow.Expires = assoc.Expires.ToLocalTime();
			assocRow.PrivateData = assoc.SerializePrivateData();
			dataSet.Association.AddAssociationRow(assocRow);
		}

		public Association GetAssociation(AssociationRelyingPartyType distinguishingFactor, SecuritySettings securitySettings) {
			// TODO: properly consider the securitySettings when picking an association to return.
			// properly escape the URL to prevent injection attacks.
			string value = distinguishingFactor.ToString();
			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(AssociationRelyingPartyType distinguishingFactor, string handle) {
			var assocRow = dataSet.Association.FindByDistinguishingFactorHandle(distinguishingFactor.ToString(), handle);
			return Association.Deserialize(assocRow.Handle, assocRow.Expires, assocRow.PrivateData);
		}

		public bool RemoveAssociation(AssociationRelyingPartyType distinguishingFactor, string handle) {
			var row = dataSet.Association.FindByDistinguishingFactorHandle(distinguishingFactor.ToString(), 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);
			}
		}

		#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;
				}

				TimeSpan maxMessageAge = DotNetOpenAuth.Configuration.DotNetOpenAuthSection.Configuration.Messaging.MaximumMessageLifetime;
				dataSet.Nonce.AddNonceRow(nonce, timestamp.ToLocalTime(), (timestamp + maxMessageAge).ToLocalTime());
				return true;
			}
		}

		public void ClearExpiredNonces() {
			this.removeExpiredRows(dataSet.Nonce, dataSet.Nonce.ExpiresColumn.ColumnName);
		}

		#endregion
	}
}