summaryrefslogtreecommitdiffstats
path: root/samples/RelyingPartyCustomStore/CustomStore.cs
blob: b0e3700d1099fc9ab451a67a3ed7278efb47bdcb (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
using System;
using System.Data;
using System.Globalization;
using System.Security.Cryptography;
using DotNetOpenId;
using DotNetOpenId.RelyingParty;

namespace RelyingPartyCustomStore {
	/// <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 {
		public static CustomStore Instance = new CustomStore();
		public CustomStoreDataSet dataSet = new CustomStoreDataSet();

		#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() {
			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);
		}

	}
}