summaryrefslogtreecommitdiffstats
path: root/projecttemplates/RelyingPartyLogic/RelyingPartyApplicationDbStore.cs
blob: 9a2f8bbbb9884c4d318167cf0bd31af58a2ef205 (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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
//-----------------------------------------------------------------------
// <copyright file="RelyingPartyApplicationDbStore.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.Linq;
	using System.Text;
	using DotNetOpenAuth.OpenId;
	using DotNetOpenAuth.OpenId.RelyingParty;

	/// <summary>
	/// A database-backed state store for OpenID relying parties.
	/// </summary>
	public class RelyingPartyApplicationDbStore : NonceDbStore, IRelyingPartyApplicationStore {
		/// <summary>
		/// Initializes a new instance of the <see cref="RelyingPartyApplicationDbStore"/> class.
		/// </summary>
		public RelyingPartyApplicationDbStore() {
		}

		#region IAssociationStore<Uri> Members

		/// <summary>
		/// Saves an <see cref="Association"/> for later recall.
		/// </summary>
		/// <param name="distinguishingFactor">The Uri (for relying parties) or Smart/Dumb (for providers).</param>
		/// <param name="association">The association to store.</param>
		/// <remarks>
		/// TODO: what should implementations do on association handle conflict?
		/// </remarks>
		public void StoreAssociation(Uri distinguishingFactor, Association association) {
			using (var dataContext = new TransactedDatabaseEntities(System.Data.IsolationLevel.ReadCommitted)) {
				var sharedAssociation = new OpenIdAssociation {
					DistinguishingFactor = distinguishingFactor.AbsoluteUri,
					AssociationHandle = association.Handle,
					ExpirationUtc = association.Expires,
					PrivateData = association.SerializePrivateData(),
				};

				dataContext.AddToOpenIdAssociations(sharedAssociation);
			}
		}

		/// <summary>
		/// Gets the best association (the one with the longest remaining life) for a given key.
		/// </summary>
		/// <param name="distinguishingFactor">The Uri (for relying parties) or Smart/Dumb (for Providers).</param>
		/// <param name="securityRequirements">The security requirements that the returned association must meet.</param>
		/// <returns>
		/// The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key.
		/// </returns>
		/// <remarks>
		/// In the event that multiple associations exist for the given
		/// <paramref name="distinguishingFactor"/>, it is important for the
		/// implementation for this method to use the <paramref name="securityRequirements"/>
		/// to pick the best (highest grade or longest living as the host's policy may dictate)
		/// association that fits the security requirements.
		/// Associations that are returned that do not meet the security requirements will be
		/// ignored and a new association created.
		/// </remarks>
		public Association GetAssociation(Uri distinguishingFactor, SecuritySettings securityRequirements) {
			using (var dataContext = new TransactedDatabaseEntities(System.Data.IsolationLevel.ReadCommitted)) {
				var relevantAssociations = from assoc in dataContext.OpenIdAssociations
										   where assoc.DistinguishingFactor == distinguishingFactor.AbsoluteUri
										   where assoc.ExpirationUtc > DateTime.UtcNow
										   where assoc.PrivateDataLength * 8 >= securityRequirements.MinimumHashBitLength
										   where assoc.PrivateDataLength * 8 <= securityRequirements.MaximumHashBitLength
										   orderby assoc.ExpirationUtc descending
										   select assoc;
				var qualifyingAssociations = relevantAssociations.AsEnumerable()
					.Select(assoc => DeserializeAssociation(assoc));
				return qualifyingAssociations.FirstOrDefault();
			}
		}

		/// <summary>
		/// Gets the association for a given key and handle.
		/// </summary>
		/// <param name="distinguishingFactor">The Uri (for relying parties) or Smart/Dumb (for Providers).</param>
		/// <param name="handle">The handle of the specific association that must be recalled.</param>
		/// <returns>
		/// The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key and handle.
		/// </returns>
		public Association GetAssociation(Uri distinguishingFactor, string handle) {
			using (var dataContext = new TransactedDatabaseEntities(System.Data.IsolationLevel.ReadCommitted)) {
				var associations = from assoc in dataContext.OpenIdAssociations
								   where assoc.DistinguishingFactor == distinguishingFactor.AbsoluteUri
								   where assoc.AssociationHandle == handle
								   where assoc.ExpirationUtc > DateTime.UtcNow
								   select assoc;
				return associations.AsEnumerable()
					.Select(assoc => DeserializeAssociation(assoc))
					.FirstOrDefault();
			}
		}

		/// <summary>
		/// Removes a specified handle that may exist in the store.
		/// </summary>
		/// <param name="distinguishingFactor">The Uri (for relying parties) or Smart/Dumb (for Providers).</param>
		/// <param name="handle">The handle of the specific association that must be deleted.</param>
		/// <returns>
		/// True if the association existed in this store previous to this call.
		/// </returns>
		/// <remarks>
		/// No exception should be thrown if the association does not exist in the store
		/// before this call.
		/// </remarks>
		public bool RemoveAssociation(Uri distinguishingFactor, string handle) {
			using (var dataContext = new TransactedDatabaseEntities(System.Data.IsolationLevel.ReadCommitted)) {
				var association = dataContext.OpenIdAssociations.FirstOrDefault(a => a.DistinguishingFactor == distinguishingFactor.AbsoluteUri && a.AssociationHandle == handle);
				if (association != null) {
					dataContext.DeleteObject(association);
					return true;
				} else {
					return false;
				}
			}
		}

		/// <summary>
		/// Clears all expired associations from the store.
		/// </summary>
		/// <remarks>
		/// If another algorithm is in place to periodically clear out expired associations,
		/// this method call may be ignored.
		/// This should be done frequently enough to avoid a memory leak, but sparingly enough
		/// to not be a performance drain.
		/// </remarks>
		public void ClearExpiredAssociations() {
			using (var dataContext = new TransactedDatabaseEntities(IsolationLevel.ReadCommitted)) {
				dataContext.ClearExpiredAssociations();
			}
		}

		#endregion

		/// <summary>
		/// Deserializes an association from the database.
		/// </summary>
		/// <param name="association">The association from the database.</param>
		/// <returns>The deserialized association.</returns>
		private static Association DeserializeAssociation(OpenIdAssociation association) {
			if (association == null) {
				throw new ArgumentNullException("association");
			}

			byte[] privateData = new byte[association.PrivateDataLength];
			Array.Copy(association.PrivateData, privateData, association.PrivateDataLength);
			return Association.Deserialize(association.AssociationHandle, association.ExpirationUtc, privateData);
		}
	}
}