summaryrefslogtreecommitdiffstats
path: root/projecttemplates/RelyingPartyLogic/NonceDbStore.cs
blob: 2f3c6707ce6ac94bf6db5aa6fed051bbf6a9f890 (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
//-----------------------------------------------------------------------
// <copyright file="NonceDbStore.cs" company="Andrew Arnott">
//     Copyright (c) Andrew Arnott. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace RelyingPartyLogic {
	using System;
	using System.Collections.Generic;
	using System.Data;
	using System.Data.Common;
	using System.Linq;
	using System.Text;
	using DotNetOpenAuth.Configuration;
	using DotNetOpenAuth.Messaging.Bindings;

	/// <summary>
	/// A database-backed nonce store for OpenID and OAuth services.
	/// </summary>
	public class NonceDbStore : INonceStore {
		private const int NonceClearingInterval = 5;

		/// <summary>
		/// A counter that tracks how many nonce stores have been done.
		/// </summary>
		private static int nonceClearingCounter;

		/// <summary>
		/// Initializes a new instance of the <see cref="NonceDbStore"/> class.
		/// </summary>
		public NonceDbStore() {
		}

		#region INonceStore Members

		/// <summary>
		/// Stores a given nonce and timestamp.
		/// </summary>
		/// <param name="context">The context, or namespace, within which the
		/// <paramref name="nonce"/> must be unique.
		/// The context SHOULD be treated as case-sensitive.
		/// The value will never be <c>null</c> but may be the empty string.</param>
		/// <param name="nonce">A series of random characters.</param>
		/// <param name="timestampUtc">The UTC timestamp that together with the nonce string make it unique
		/// within the given <paramref name="context"/>.
		/// The timestamp may also be used by the data store to clear out old nonces.</param>
		/// <returns>
		/// True if the context+nonce+timestamp (combination) was not previously in the database.
		/// False if the nonce was stored previously with the same timestamp and context.
		/// </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.
		/// This maximum message age can be looked up via the
		/// <see cref="DotNetOpenAuth.Configuration.MessagingElement.MaximumMessageLifetime"/>
		/// property, accessible via the <see cref="DotNetOpenAuth.Configuration.DotNetOpenAuthSection.Configuration"/>
		/// property.
		/// </remarks>
		public bool StoreNonce(string context, string nonce, DateTime timestampUtc) {
			try {
				using (var dataContext = new TransactedDatabaseEntities(IsolationLevel.ReadCommitted)) {
					Nonce nonceEntity = new Nonce {
						Context = context,
						Code = nonce,
						IssuedUtc = timestampUtc,
						ExpiresUtc = timestampUtc + DotNetOpenAuthSection.Configuration.Messaging.MaximumMessageLifetime,
					};

					// The database columns [context] and [code] MUST be using
					// a case sensitive collation for this to be secure.
					dataContext.AddToNonces(nonceEntity);
				}
			} catch (UpdateException) {
				// A nonce collision
				return false;
			}

			// Only clear nonces after successfully storing a nonce.
			// This mitigates cheap DoS attacks that take up a lot of
			// database cycles.
			ClearNoncesIfAppropriate();
			return true;
		}

		#endregion

		/// <summary>
		/// Clears the nonces if appropriate.
		/// </summary>
		private static void ClearNoncesIfAppropriate() {
			if (++nonceClearingCounter % NonceClearingInterval == 0) {
				using (var dataContext = new TransactedDatabaseEntities(IsolationLevel.ReadCommitted)) {
					dataContext.ClearExpiredNonces();
				}
			}
		}

		/// <summary>
		/// A transacted data context.
		/// </summary>
		protected class TransactedDatabaseEntities : DatabaseEntities {
			/// <summary>
			/// The transaction for this data context.
			/// </summary>
			private DbTransaction transaction;

			/// <summary>
			/// Initializes a new instance of the <see cref="TransactedDatabaseEntities"/> class.
			/// </summary>
			/// <param name="isolationLevel">The isolation level.</param>
			public TransactedDatabaseEntities(IsolationLevel isolationLevel) {
				this.Connection.Open();
				this.transaction = this.Connection.BeginTransaction(isolationLevel);
			}

			/// <summary>
			/// Releases the resources used by the object context.
			/// </summary>
			/// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
			protected override void Dispose(bool disposing) {
				try {
					this.SaveChanges();
					this.transaction.Commit();
				} finally {
					this.Connection.Close();
				}

				base.Dispose(disposing);
			}
		}
	}
}