summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj1
-rw-r--r--samples/DotNetOpenAuth.ApplicationBlock/Provider/AnonymousIdentifierProviderBase.cs122
-rw-r--r--samples/DotNetOpenAuth.ApplicationBlock/Provider/AuthenticationRequestExtensions.cs5
-rw-r--r--samples/DotNetOpenAuth.ApplicationBlock/Util.cs13
-rw-r--r--samples/OpenIdProviderMvc/Code/AnonymousIdentifierProvider.cs3
-rw-r--r--samples/OpenIdProviderMvc/Controllers/OpenIdController.cs2
-rw-r--r--src/DotNetOpenAuth/DotNetOpenAuth.csproj2
-rw-r--r--src/DotNetOpenAuth/Messaging/MessagingUtilities.cs16
-rw-r--r--src/DotNetOpenAuth/OpenId/Provider/IDirectedIdentityCustomIdentifier.cs60
-rw-r--r--src/DotNetOpenAuth/OpenId/Provider/IHostProcessedRequest.cs100
-rw-r--r--src/DotNetOpenAuth/OpenId/Provider/PrivatePersonalIdentifierProviderBase.cs176
11 files changed, 359 insertions, 141 deletions
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj b/samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj
index 570d91f..b35fa29 100644
--- a/samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj
+++ b/samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj
@@ -62,7 +62,6 @@
<Compile Include="OAuthIdentity.cs" />
<Compile Include="OAuthPrincipal.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
- <Compile Include="Provider\AnonymousIdentifierProviderBase.cs" />
<Compile Include="Provider\AuthenticationRequestExtensions.cs" />
<Compile Include="TwitterConsumer.cs" />
<Compile Include="Util.cs" />
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/Provider/AnonymousIdentifierProviderBase.cs b/samples/DotNetOpenAuth.ApplicationBlock/Provider/AnonymousIdentifierProviderBase.cs
deleted file mode 100644
index 1df7267..0000000
--- a/samples/DotNetOpenAuth.ApplicationBlock/Provider/AnonymousIdentifierProviderBase.cs
+++ /dev/null
@@ -1,122 +0,0 @@
-//-----------------------------------------------------------------------
-// <copyright file="AnonymousIdentifierProviderBase.cs" company="Andrew Arnott">
-// Copyright (c) Andrew Arnott. All rights reserved.
-// </copyright>
-//-----------------------------------------------------------------------
-
-namespace DotNetOpenAuth.ApplicationBlock.Provider {
- using System;
- using System.Collections.Generic;
- using System.Diagnostics.CodeAnalysis;
- using System.Linq;
- using System.Security.Cryptography;
- using System.Text;
- using DotNetOpenAuth.Messaging;
- using DotNetOpenAuth.OpenId;
-
- public abstract class AnonymousIdentifierProviderBase {
- private int newSaltLength = 20;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="AnonymousIdentifierProviderBase"/> class.
- /// </summary>
- /// <param name="baseIdentifier">The base URI on which to append the anonymous part.</param>
- public AnonymousIdentifierProviderBase(Uri baseIdentifier) {
- if (baseIdentifier == null) {
- throw new ArgumentNullException("baseIdentifier");
- }
-
- this.Hasher = HashAlgorithm.Create("SHA256");
- this.Encoder = Encoding.UTF8;
- this.BaseIdentifier = baseIdentifier;
- }
-
- public Uri BaseIdentifier { get; private set; }
-
- protected HashAlgorithm Hasher { get; private set; }
-
- protected Encoding Encoder { get; private set; }
-
- protected int NewSaltLength {
- get {
- return this.newSaltLength;
- }
-
- set {
- if (value <= 0) {
- throw new ArgumentOutOfRangeException("value");
- }
-
- this.newSaltLength = value;
- }
- }
-
- #region IAnonymousIdentifierProvider Members
-
- public Uri GetAnonymousIdentifier(Identifier localIdentifier, Realm relyingPartyRealm) {
- byte[] salt = this.GetHashSaltForLocalIdentifier(localIdentifier);
- string valueToHash = localIdentifier + "#" + (relyingPartyRealm ?? string.Empty);
- byte[] valueAsBytes = this.Encoder.GetBytes(valueToHash);
- byte[] bytesToHash = new byte[valueAsBytes.Length + salt.Length];
- valueAsBytes.CopyTo(bytesToHash, 0);
- salt.CopyTo(bytesToHash, valueAsBytes.Length);
- byte[] hash = this.Hasher.ComputeHash(bytesToHash);
- string base64Hash = Convert.ToBase64String(hash);
- Uri anonymousIdentifier = this.AppendIdentifiers(this.BaseIdentifier, base64Hash);
- return anonymousIdentifier;
- }
-
- #endregion
-
- protected virtual byte[] GetNewSalt() {
- // We COULD use a crypto random function, but for a salt it seems overkill.
- return Util.GetNonCryptoRandomData(this.NewSaltLength);
- }
-
- protected Uri AppendIdentifiers(Uri baseIdentifier, string uriHash) {
- if (baseIdentifier == null) {
- throw new ArgumentNullException("baseIdentifier");
- }
- if (String.IsNullOrEmpty(uriHash)) {
- throw new ArgumentNullException("uriHash");
- }
-
- if (string.IsNullOrEmpty(baseIdentifier.Query)) {
- // The uriHash will appear on the path itself.
- string pathEncoded = Uri.EscapeUriString(uriHash.Replace('/', '_'));
- return new Uri(baseIdentifier, pathEncoded);
- } else {
- // The uriHash will appear on the query string.
- string dataEncoded = Uri.EscapeDataString(uriHash);
- return new Uri(baseIdentifier + dataEncoded);
- }
- }
-
- /// <summary>
- /// Gets the salt to use for generating an anonymous identifier for a given OP local identifier.
- /// </summary>
- /// <param name="localIdentifier">The OP local identifier.</param>
- /// <returns>The salt to use in the hash.</returns>
- /// <remarks>
- /// It is important that this method always return the same value for a given
- /// <paramref name="localIdentifier"/>.
- /// New salts can be generated for local identifiers without previously assigned salt
- /// values by calling <see cref="GetNewSalt"/> or by a custom method.
- /// </remarks>
- protected abstract byte[] GetHashSaltForLocalIdentifier(Identifier localIdentifier);
-
-#if CONTRACTS_FULL
- /// <summary>
- /// Verifies conditions that should be true for any valid state of this object.
- /// </summary>
- [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")]
- [ContractInvariantMethod]
- protected void ObjectInvariant() {
- Contract.Invariant(this.Hasher != null);
- Contract.Invariant(this.Encoder != null);
- Contract.Invariant(this.BaseIdentifier != null);
- Contract.Invariant(this.NewHashLength > 0);
- }
-#endif
- }
-}
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/Provider/AuthenticationRequestExtensions.cs b/samples/DotNetOpenAuth.ApplicationBlock/Provider/AuthenticationRequestExtensions.cs
index a737d30..8af72aa 100644
--- a/samples/DotNetOpenAuth.ApplicationBlock/Provider/AuthenticationRequestExtensions.cs
+++ b/samples/DotNetOpenAuth.ApplicationBlock/Provider/AuthenticationRequestExtensions.cs
@@ -10,11 +10,10 @@
/// <param name="request">The incoming authentication request.</param>
/// <param name="localIdentifier">The OP local identifier, before the anonymous hash is applied to it.</param>
/// <param name="anonymousIdentifierProvider">The anonymous identifier provider.</param>
- /// <param name="pairwiseUnique">if set to <c>true</c> the anonymous identifier will be unique to the requesting relying party's realm.</param>
/// <remarks>
/// The openid.claimed_id and openid.identity values are hashed.
/// </remarks>
- public static void ScrubPersonallyIdentifiableInformation(this IAuthenticationRequest request, Identifier localIdentifier, AnonymousIdentifierProviderBase anonymousIdentifierProvider, bool pairwiseUnique) {
+ public static void ScrubPersonallyIdentifiableInformation(this IAuthenticationRequest request, Identifier localIdentifier, IDirectedIdentityIdentifierProvider anonymousIdentifierProvider) {
if (request == null) {
throw new ArgumentNullException("request");
}
@@ -30,7 +29,7 @@
// When generating the anonymous identifiers, the openid.identity and openid.claimed_id
// will always end up with matching values.
- var anonymousIdentifier = anonymousIdentifierProvider.GetAnonymousIdentifier(localIdentifier, pairwiseUnique ? request.Realm : null);
+ var anonymousIdentifier = anonymousIdentifierProvider.GetIdentifier(localIdentifier, request.Realm);
request.ClaimedIdentifier = anonymousIdentifier;
request.LocalIdentifier = anonymousIdentifier;
}
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/Util.cs b/samples/DotNetOpenAuth.ApplicationBlock/Util.cs
index 8a188ac..ea7da97 100644
--- a/samples/DotNetOpenAuth.ApplicationBlock/Util.cs
+++ b/samples/DotNetOpenAuth.ApplicationBlock/Util.cs
@@ -5,8 +5,6 @@
using DotNetOpenAuth.Messaging;
internal static class Util {
- internal static readonly Random NonCryptoRandomDataGenerator = new Random();
-
/// <summary>
/// Enumerates through the individual set bits in a flag enum.
/// </summary>
@@ -30,17 +28,6 @@
}
/// <summary>
- /// Gets a buffer of random data (not cryptographically strong).
- /// </summary>
- /// <param name="length">The length of the sequence to generate.</param>
- /// <returns>The generated values, which may contain zeros.</returns>
- internal static byte[] GetNonCryptoRandomData(int length) {
- byte[] buffer = new byte[length];
- NonCryptoRandomDataGenerator.NextBytes(buffer);
- return buffer;
- }
-
- /// <summary>
/// Copies the contents of one stream to another.
/// </summary>
/// <param name="copyFrom">The stream to copy from, at the position where copying should begin.</param>
diff --git a/samples/OpenIdProviderMvc/Code/AnonymousIdentifierProvider.cs b/samples/OpenIdProviderMvc/Code/AnonymousIdentifierProvider.cs
index 2b9e01c..bed4e82 100644
--- a/samples/OpenIdProviderMvc/Code/AnonymousIdentifierProvider.cs
+++ b/samples/OpenIdProviderMvc/Code/AnonymousIdentifierProvider.cs
@@ -3,9 +3,10 @@
using System.Web.Security;
using DotNetOpenAuth.ApplicationBlock.Provider;
using DotNetOpenAuth.OpenId;
+ using DotNetOpenAuth.OpenId.Provider;
using OpenIdProviderMvc.Models;
- internal class AnonymousIdentifierProvider : AnonymousIdentifierProviderBase {
+ internal class AnonymousIdentifierProvider : PrivatePersonalIdentifierProviderBase {
internal AnonymousIdentifierProvider()
: base(Util.GetAppPathRootedUri("anon?id=")) {
}
diff --git a/samples/OpenIdProviderMvc/Controllers/OpenIdController.cs b/samples/OpenIdProviderMvc/Controllers/OpenIdController.cs
index e353268..8aad0ba 100644
--- a/samples/OpenIdProviderMvc/Controllers/OpenIdController.cs
+++ b/samples/OpenIdProviderMvc/Controllers/OpenIdController.cs
@@ -45,7 +45,7 @@ namespace OpenIdProviderMvc.Controllers {
}
var anonProvider = new AnonymousIdentifierProvider();
- authReq.ScrubPersonallyIdentifiableInformation(localIdentifier, anonProvider, true);
+ authReq.ScrubPersonallyIdentifiableInformation(localIdentifier, anonProvider);
authReq.IsAuthenticated = true;
} else {
if (authReq.IsDirectedIdentity) {
diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj
index 02e093a..bc10d44 100644
--- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj
+++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj
@@ -373,6 +373,7 @@
<Compile Include="OpenId\Messages\SignedResponseRequest.cs" />
<Compile Include="OpenId\NoDiscoveryIdentifier.cs" />
<Compile Include="OpenId\OpenIdUtilities.cs" />
+ <Compile Include="OpenId\Provider\PrivatePersonalIdentifierProviderBase.cs" />
<Compile Include="OpenId\Provider\AnonymousRequest.cs" />
<Compile Include="OpenId\Provider\AnonymousRequestEventArgs.cs" />
<Compile Include="OpenId\Provider\AuthenticationChallengeEventArgs.cs" />
@@ -381,6 +382,7 @@
<Compile Include="OpenId\Provider\HostProcessedRequest.cs" />
<Compile Include="OpenId\Provider\IAnonymousRequest.cs" />
<Compile Include="OpenId\Provider\IAuthenticationRequest.cs" />
+ <Compile Include="OpenId\Provider\IDirectedIdentityCustomIdentifier.cs" />
<Compile Include="OpenId\Provider\IHostProcessedRequest.cs" />
<Compile Include="OpenId\Provider\IdentityEndpoint.cs" />
<Compile Include="OpenId\Provider\IdentityEndpointNormalizationEventArgs.cs" />
diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs
index 10ddba0..b06502e 100644
--- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs
+++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs
@@ -31,6 +31,11 @@ namespace DotNetOpenAuth.Messaging {
internal static readonly RandomNumberGenerator CryptoRandomDataGenerator = new RNGCryptoServiceProvider();
/// <summary>
+ /// A pseudo-random data generator (NOT cryptographically strong random data)
+ /// </summary>
+ internal static readonly Random NonCryptoRandomDataGenerator = new Random();
+
+ /// <summary>
/// The set of characters that are unreserved in RFC 2396 but are NOT unreserved in RFC 3986.
/// </summary>
private static readonly string[] UriRfc3986CharsToEscape = new[] { "!", "*", "'", "(", ")" };
@@ -140,6 +145,17 @@ namespace DotNetOpenAuth.Messaging {
}
/// <summary>
+ /// Gets a buffer of random data (not cryptographically strong).
+ /// </summary>
+ /// <param name="length">The length of the sequence to generate.</param>
+ /// <returns>The generated values, which may contain zeros.</returns>
+ internal static byte[] GetNonCryptoRandomData(int length) {
+ byte[] buffer = new byte[length];
+ NonCryptoRandomDataGenerator.NextBytes(buffer);
+ return buffer;
+ }
+
+ /// <summary>
/// Gets a cryptographically strong random sequence of values.
/// </summary>
/// <param name="length">The length of the sequence to generate.</param>
diff --git a/src/DotNetOpenAuth/OpenId/Provider/IDirectedIdentityCustomIdentifier.cs b/src/DotNetOpenAuth/OpenId/Provider/IDirectedIdentityCustomIdentifier.cs
new file mode 100644
index 0000000..63f9cdf
--- /dev/null
+++ b/src/DotNetOpenAuth/OpenId/Provider/IDirectedIdentityCustomIdentifier.cs
@@ -0,0 +1,60 @@
+//-----------------------------------------------------------------------
+// <copyright file="IDirectedIdentityCustomIdentifier.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ using System;
+ using System.Diagnostics.Contracts;
+
+ /// <summary>
+ /// An interface to provide custom identifiers for users logging into specific relying parties.
+ /// </summary>
+ /// <remarks>
+ /// This interface would allow, for example, the Provider to offer PPIDs to their users,
+ /// allowing the users to log into RPs without leaving any clue as to their true identity,
+ /// and preventing multiple RPs from colluding to track user activity across realms.
+ /// </remarks>
+ [ContractClass(typeof(IDirectedIdentityIdentifierProviderContract))]
+ public interface IDirectedIdentityIdentifierProvider {
+ /// <summary>
+ /// Gets the Identifier to use for the Claimed Identifier and Local Identifier of
+ /// an outgoing positive assertion.
+ /// </summary>
+ /// <param name="localIdentifier">The OP local identifier for the authenticating user.</param>
+ /// <param name="relyingPartyRealm">The realm of the relying party receiving the assertion.</param>
+ /// <returns>
+ /// A valid, discoverable OpenID Identifier that should be used as the value for the
+ /// openid.claimed_id and openid.local_id parameters. Must not be null.
+ /// </returns>
+ Uri GetIdentifier(Identifier localIdentifier, Realm relyingPartyRealm);
+ }
+
+ /// <summary>
+ /// Contract class for the <see cref="IDirectedIdentityIdentifierProvider"/> type.
+ /// </summary>
+ [ContractClassFor(typeof(IDirectedIdentityIdentifierProvider))]
+ internal abstract class IDirectedIdentityIdentifierProviderContract : IDirectedIdentityIdentifierProvider {
+ #region IDirectedIdentityIdentifierProvider Members
+
+ /// <summary>
+ /// Gets the Identifier to use for the Claimed Identifier and Local Identifier of
+ /// an outgoing positive assertion.
+ /// </summary>
+ /// <param name="localIdentifier">The OP local identifier for the authenticating user.</param>
+ /// <param name="relyingPartyRealm">The realm of the relying party receiving the assertion.</param>
+ /// <returns>
+ /// A valid, discoverable OpenID Identifier that should be used as the value for the
+ /// openid.claimed_id and openid.local_id parameters. Must not be null.
+ /// </returns>
+ Uri IDirectedIdentityIdentifierProvider.GetIdentifier(Identifier localIdentifier, Realm relyingPartyRealm) {
+ Contract.Requires(localIdentifier != null);
+ Contract.Requires(relyingPartyRealm != null);
+
+ throw new NotImplementedException();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth/OpenId/Provider/IHostProcessedRequest.cs b/src/DotNetOpenAuth/OpenId/Provider/IHostProcessedRequest.cs
index 3e743ec..eee99f1 100644
--- a/src/DotNetOpenAuth/OpenId/Provider/IHostProcessedRequest.cs
+++ b/src/DotNetOpenAuth/OpenId/Provider/IHostProcessedRequest.cs
@@ -5,12 +5,14 @@
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.OpenId.Provider {
+ using System.Diagnostics.Contracts;
using DotNetOpenAuth.Messaging;
/// <summary>
/// Interface exposing incoming messages to the OpenID Provider that
/// require interaction with the host site.
/// </summary>
+ [ContractClass(typeof(IHostProcessedRequestContract))]
public interface IHostProcessedRequest : IRequest {
/// <summary>
/// Gets the version of OpenID being used by the relying party that sent the request.
@@ -42,4 +44,102 @@ namespace DotNetOpenAuth.OpenId.Provider {
/// </remarks>
RelyingPartyDiscoveryResult IsReturnUrlDiscoverable(OpenIdProvider provider);
}
+
+ /// <summary>
+ /// Contract class for the <see cref="IHostProcessedRequest"/> type.
+ /// </summary>
+ [ContractClassFor(typeof(IHostProcessedRequest))]
+ internal abstract class IHostProcessedRequestContract : IHostProcessedRequest {
+ #region IHostProcessedRequest Properties
+
+ /// <summary>
+ /// Gets the version of OpenID being used by the relying party that sent the request.
+ /// </summary>
+ ProtocolVersion IHostProcessedRequest.RelyingPartyVersion {
+ get { throw new System.NotImplementedException(); }
+ }
+
+ /// <summary>
+ /// Gets the URL the consumer site claims to use as its 'base' address.
+ /// </summary>
+ Realm IHostProcessedRequest.Realm {
+ get { throw new System.NotImplementedException(); }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the consumer demands an immediate response.
+ /// If false, the consumer is willing to wait for the identity provider
+ /// to authenticate the user.
+ /// </summary>
+ bool IHostProcessedRequest.Immediate {
+ get { throw new System.NotImplementedException(); }
+ }
+
+ #endregion
+
+ #region IRequest Members
+
+ /// <summary>
+ /// Gets a value indicating whether the response is ready to be sent to the user agent.
+ /// </summary>
+ /// <remarks>
+ /// This property returns false if there are properties that must be set on this
+ /// request instance before the response can be sent.
+ /// </remarks>
+ bool IRequest.IsResponseReady {
+ get { throw new System.NotImplementedException(); }
+ }
+
+ /// <summary>
+ /// Adds an extension to the response to send to the relying party.
+ /// </summary>
+ /// <param name="extension">The extension to add to the response message.</param>
+ void IRequest.AddResponseExtension(DotNetOpenAuth.OpenId.Messages.IOpenIdMessageExtension extension) {
+ throw new System.NotImplementedException();
+ }
+
+ /// <summary>
+ /// Gets an extension sent from the relying party.
+ /// </summary>
+ /// <typeparam name="T">The type of the extension.</typeparam>
+ /// <returns>
+ /// An instance of the extension initialized with values passed in with the request.
+ /// </returns>
+ T IRequest.GetExtension<T>() {
+ throw new System.NotImplementedException();
+ }
+
+ /// <summary>
+ /// Gets an extension sent from the relying party.
+ /// </summary>
+ /// <param name="extensionType">The type of the extension.</param>
+ /// <returns>
+ /// An instance of the extension initialized with values passed in with the request.
+ /// </returns>
+ DotNetOpenAuth.OpenId.Messages.IOpenIdMessageExtension IRequest.GetExtension(System.Type extensionType) {
+ throw new System.NotImplementedException();
+ }
+
+ #endregion
+
+ #region IHostProcessedRequest Methods
+
+ /// <summary>
+ /// Attempts to perform relying party discovery of the return URL claimed by the Relying Party.
+ /// </summary>
+ /// <param name="provider">The OpenIdProvider that is performing the RP discovery.</param>
+ /// <returns>
+ /// The details of how successful the relying party discovery was.
+ /// </returns>
+ /// <remarks>
+ /// <para>Return URL verification is only attempted if this method is called.</para>
+ /// <para>See OpenID Authentication 2.0 spec section 9.2.1.</para>
+ /// </remarks>
+ RelyingPartyDiscoveryResult IHostProcessedRequest.IsReturnUrlDiscoverable(OpenIdProvider provider) {
+ Contract.Requires(provider != null);
+ throw new System.NotImplementedException();
+ }
+
+ #endregion
+ }
}
diff --git a/src/DotNetOpenAuth/OpenId/Provider/PrivatePersonalIdentifierProviderBase.cs b/src/DotNetOpenAuth/OpenId/Provider/PrivatePersonalIdentifierProviderBase.cs
new file mode 100644
index 0000000..a4b123b
--- /dev/null
+++ b/src/DotNetOpenAuth/OpenId/Provider/PrivatePersonalIdentifierProviderBase.cs
@@ -0,0 +1,176 @@
+//-----------------------------------------------------------------------
+// <copyright file="PrivatePersonalIdentifierProviderBase.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Provider {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Security.Cryptography;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// Provides standard PPID Identifiers to users to protect their identity from individual relying parties
+ /// and from colluding groups of relying parties.
+ /// </summary>
+ public abstract class PrivatePersonalIdentifierProviderBase : IDirectedIdentityIdentifierProvider {
+ /// <summary>
+ /// The type of hash function to use for the <see cref="Hasher"/> property.
+ /// </summary>
+ private const string HashAlgorithmName = "SHA256";
+
+ /// <summary>
+ /// The length of the salt to generate for first time PPID-users.
+ /// </summary>
+ private int newSaltLength = 20;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PrivatePersonalIdentifierProviderBase"/> class.
+ /// </summary>
+ /// <param name="baseIdentifier">The base URI on which to append the anonymous part.</param>
+ public PrivatePersonalIdentifierProviderBase(Uri baseIdentifier) {
+ Contract.Requires(baseIdentifier != null);
+ ErrorUtilities.VerifyArgumentNotNull(baseIdentifier, "baseIdentifier");
+
+ this.Hasher = HashAlgorithm.Create(HashAlgorithmName);
+ this.Encoder = Encoding.UTF8;
+ this.BaseIdentifier = baseIdentifier;
+ this.PairwiseUnique = true;
+ }
+
+ /// <summary>
+ /// Gets the base URI on which to append the anonymous part.
+ /// </summary>
+ public Uri BaseIdentifier { get; private set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether each Realm will get its own private identifier
+ /// for the authenticating uesr.
+ /// </summary>
+ /// <value>The default value is <c>true</c>.</value>
+ public bool PairwiseUnique { get; set; }
+
+ /// <summary>
+ /// Gets the hash function to use to perform the one-way transform of a personal identifier
+ /// to an "anonymous" looking one.
+ /// </summary>
+ protected HashAlgorithm Hasher { get; private set; }
+
+ /// <summary>
+ /// Gets the encoder to use for transforming the personal identifier into bytes for hashing.
+ /// </summary>
+ protected Encoding Encoder { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the new length of the salt.
+ /// </summary>
+ /// <value>The new length of the salt.</value>
+ protected int NewSaltLength {
+ get {
+ return this.newSaltLength;
+ }
+
+ set {
+ Contract.Requires(value > 0);
+ ErrorUtilities.VerifyArgumentInRange(value > 0, "value");
+ this.newSaltLength = value;
+ }
+ }
+
+ #region IDirectedIdentityIdentifierProvider Members
+
+ /// <summary>
+ /// Gets the Identifier to use for the Claimed Identifier and Local Identifier of
+ /// an outgoing positive assertion.
+ /// </summary>
+ /// <param name="localIdentifier">The OP local identifier for the authenticating user.</param>
+ /// <param name="relyingPartyRealm">The realm of the relying party receiving the assertion.</param>
+ /// <returns>
+ /// A valid, discoverable OpenID Identifier that should be used as the value for the
+ /// openid.claimed_id and openid.local_id parameters. Must not be null.
+ /// </returns>
+ public Uri GetIdentifier(Identifier localIdentifier, Realm relyingPartyRealm) {
+ ErrorUtilities.VerifyArgumentNotNull(localIdentifier, "localIdentifier");
+ ErrorUtilities.VerifyArgumentNotNull(relyingPartyRealm, "relyingPartyRealm");
+
+ byte[] salt = this.GetHashSaltForLocalIdentifier(localIdentifier);
+ string valueToHash = localIdentifier + "#";
+ if (this.PairwiseUnique) {
+ valueToHash += relyingPartyRealm;
+ }
+
+ byte[] valueAsBytes = this.Encoder.GetBytes(valueToHash);
+ byte[] bytesToHash = new byte[valueAsBytes.Length + salt.Length];
+ valueAsBytes.CopyTo(bytesToHash, 0);
+ salt.CopyTo(bytesToHash, valueAsBytes.Length);
+ byte[] hash = this.Hasher.ComputeHash(bytesToHash);
+ string base64Hash = Convert.ToBase64String(hash);
+ Uri anonymousIdentifier = this.AppendIdentifiers(base64Hash);
+ return anonymousIdentifier;
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Creates a new salt to assign to a user.
+ /// </summary>
+ /// <returns>A non-null buffer of length <see cref="NewSaltLength"/> filled with a random salt.</returns>
+ protected virtual byte[] CreateSalt() {
+ // We COULD use a crypto random function, but for a salt it seems overkill.
+ return MessagingUtilities.GetNonCryptoRandomData(this.NewSaltLength);
+ }
+
+ /// <summary>
+ /// Creates a new PPID Identifier by appending a pseudonymous identifier suffix to
+ /// the <see cref="BaseIdentifier"/>.
+ /// </summary>
+ /// <param name="uriHash">The unique part of the Identifier to append to the common first part.</param>
+ /// <returns>The full PPID Identifier.</returns>
+ protected virtual Uri AppendIdentifiers(string uriHash) {
+ Contract.Requires(!String.IsNullOrEmpty(uriHash));
+ ErrorUtilities.VerifyNonZeroLength(uriHash, "uriHash");
+
+ if (string.IsNullOrEmpty(this.BaseIdentifier.Query)) {
+ // The uriHash will appear on the path itself.
+ string pathEncoded = Uri.EscapeUriString(uriHash.Replace('/', '_'));
+ return new Uri(this.BaseIdentifier, pathEncoded);
+ } else {
+ // The uriHash will appear on the query string.
+ string dataEncoded = Uri.EscapeDataString(uriHash);
+ return new Uri(this.BaseIdentifier + dataEncoded);
+ }
+ }
+
+ /// <summary>
+ /// Gets the salt to use for generating an anonymous identifier for a given OP local identifier.
+ /// </summary>
+ /// <param name="localIdentifier">The OP local identifier.</param>
+ /// <returns>The salt to use in the hash.</returns>
+ /// <remarks>
+ /// It is important that this method always return the same value for a given
+ /// <paramref name="localIdentifier"/>.
+ /// New salts can be generated for local identifiers without previously assigned salt
+ /// values by calling <see cref="CreateSalt"/> or by a custom method.
+ /// </remarks>
+ protected abstract byte[] GetHashSaltForLocalIdentifier(Identifier localIdentifier);
+
+#if CONTRACTS_FULL
+ /// <summary>
+ /// Verifies conditions that should be true for any valid state of this object.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")]
+ [ContractInvariantMethod]
+ protected void ObjectInvariant() {
+ Contract.Invariant(this.Hasher != null);
+ Contract.Invariant(this.Encoder != null);
+ Contract.Invariant(this.BaseIdentifier != null);
+ Contract.Invariant(this.NewSaltLength > 0);
+ }
+#endif
+ }
+}