//-----------------------------------------------------------------------
//
// Copyright (c) Andrew Arnott. All rights reserved.
//
//-----------------------------------------------------------------------
namespace RelyingPartyLogic {
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using DotNetOpenAuth.OpenId;
using DotNetOpenAuth.OpenId.RelyingParty;
///
/// A database-backed state store for OpenID relying parties.
///
public class RelyingPartyApplicationDbStore : NonceDbStore, IRelyingPartyApplicationStore {
///
/// Initializes a new instance of the class.
///
public RelyingPartyApplicationDbStore() {
}
#region IAssociationStore Members
///
/// Saves an for later recall.
///
/// The Uri (for relying parties) or Smart/Dumb (for providers).
/// The association to store.
///
/// TODO: what should implementations do on association handle conflict?
///
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);
}
}
///
/// Gets the best association (the one with the longest remaining life) for a given key.
///
/// The Uri (for relying parties) or Smart/Dumb (for Providers).
/// The security requirements that the returned association must meet.
///
/// The requested association, or null if no unexpired s exist for the given key.
///
///
/// In the event that multiple associations exist for the given
/// , it is important for the
/// implementation for this method to use the
/// 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.
///
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();
}
}
///
/// Gets the association for a given key and handle.
///
/// The Uri (for relying parties) or Smart/Dumb (for Providers).
/// The handle of the specific association that must be recalled.
///
/// The requested association, or null if no unexpired s exist for the given key and handle.
///
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();
}
}
///
/// Removes a specified handle that may exist in the store.
///
/// The Uri (for relying parties) or Smart/Dumb (for Providers).
/// The handle of the specific association that must be deleted.
///
/// True if the association existed in this store previous to this call.
///
///
/// No exception should be thrown if the association does not exist in the store
/// before this call.
///
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;
}
}
}
///
/// Clears all expired associations from the store.
///
///
/// 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.
///
public void ClearExpiredAssociations() {
using (var dataContext = new TransactedDatabaseEntities(IsolationLevel.ReadCommitted)) {
dataContext.ClearExpiredAssociations(dataContext.Transaction);
}
}
#endregion
///
/// Deserializes an association from the database.
///
/// The association from the database.
/// The deserialized association.
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);
}
}
}