summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--projecttemplates/RelyingPartyLogic/OAuthAuthorizationServer.cs31
-rw-r--r--samples/OAuthAuthorizationServer/Code/DataClasses.dbml8
-rw-r--r--samples/OAuthAuthorizationServer/Code/DataClasses.dbml.layout6
-rw-r--r--samples/OAuthAuthorizationServer/Code/DataClasses.designer.cs147
-rw-r--r--samples/OAuthAuthorizationServer/Code/DatabaseKeyNonceStore.cs (renamed from samples/OAuthAuthorizationServer/Code/DatabaseNonceStore.cs)46
-rw-r--r--samples/OAuthAuthorizationServer/Code/OAuth2AuthorizationServer.cs35
-rw-r--r--samples/OAuthAuthorizationServer/Code/Utilities.cs21
-rw-r--r--samples/OAuthAuthorizationServer/Controllers/OAuthController.cs20
-rw-r--r--samples/OAuthAuthorizationServer/Global.asax.cs4
-rw-r--r--samples/OAuthAuthorizationServer/OAuthAuthorizationServer.csproj3
-rw-r--r--src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd2
-rw-r--r--src/DotNetOpenAuth/Configuration/MessagingElement.cs4
-rw-r--r--src/DotNetOpenAuth/Configuration/OpenIdProviderSecuritySettingsElement.cs3
-rw-r--r--src/DotNetOpenAuth/Messaging/BinaryDataBagFormatter.cs10
-rw-r--r--src/DotNetOpenAuth/Messaging/Bindings/CryptoKeyCollisionException.cs4
-rw-r--r--src/DotNetOpenAuth/Messaging/DataBagFormatterBase.cs98
-rw-r--r--src/DotNetOpenAuth/Messaging/MessagingUtilities.cs78
-rw-r--r--src/DotNetOpenAuth/Messaging/UriStyleMessageFormatter.cs10
-rw-r--r--src/DotNetOpenAuth/OAuth2/AuthorizationServer.cs2
-rw-r--r--src/DotNetOpenAuth/OAuth2/ChannelElements/AccessRequestBindingElement.cs8
-rw-r--r--src/DotNetOpenAuth/OAuth2/ChannelElements/AuthorizationCode.cs18
-rw-r--r--src/DotNetOpenAuth/OAuth2/ChannelElements/RefreshToken.cs19
-rw-r--r--src/DotNetOpenAuth/OAuth2/IAuthorizationServer.cs20
-rw-r--r--src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs5
-rw-r--r--src/DotNetOpenAuth/OpenId/Provider/AssociationDataBag.cs17
-rw-r--r--src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs1
-rw-r--r--src/DotNetOpenAuth/OpenId/Provider/ProviderAssociationHandleEncoder.cs29
-rw-r--r--src/DotNetOpenAuth/OpenId/Provider/StandardProviderApplicationStore.cs33
28 files changed, 501 insertions, 181 deletions
diff --git a/projecttemplates/RelyingPartyLogic/OAuthAuthorizationServer.cs b/projecttemplates/RelyingPartyLogic/OAuthAuthorizationServer.cs
index b1f9787..f0608d5 100644
--- a/projecttemplates/RelyingPartyLogic/OAuthAuthorizationServer.cs
+++ b/projecttemplates/RelyingPartyLogic/OAuthAuthorizationServer.cs
@@ -9,9 +9,9 @@ namespace RelyingPartyLogic {
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
+ using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Web;
-
using DotNetOpenAuth.Messaging.Bindings;
using DotNetOpenAuth.OAuth2;
using DotNetOpenAuth.OAuth2.ChannelElements;
@@ -23,30 +23,18 @@ namespace RelyingPartyLogic {
public class OAuthAuthorizationServer : IAuthorizationServer {
private static readonly RSAParameters AsymmetricKey = CreateRSAKey();
- private static readonly byte[] secret = CreateSecret();
-
private readonly INonceStore nonceStore = new NonceDbStore();
/// <summary>
/// Initializes a new instance of the <see cref="OAuthAuthorizationServer"/> class.
/// </summary>
public OAuthAuthorizationServer() {
+ this.CryptoKeyStore = new RelyingPartyApplicationDbStore();
}
#region IAuthorizationServer Members
- /// <summary>
- /// Gets the secret used to symmetrically encrypt and sign authorization codes and refresh tokens.
- /// </summary>
- /// <value></value>
- /// <remarks>
- /// This secret should be kept strictly confidential in the authorization server(s)
- /// and NOT shared with the resource server. Anyone with this secret can mint
- /// tokens to essentially grant themselves access to anything they want.
- /// </remarks>
- public byte[] Secret {
- get { return secret; }
- }
+ public ICryptoKeyStore CryptoKeyStore { get; private set; }
/// <summary>
/// Gets the authorization code nonce store to use to ensure that authorization codes can only be used once.
@@ -142,19 +130,6 @@ namespace RelyingPartyLogic {
}
/// <summary>
- /// Creates a symmetric secret used to sign and encrypt authorization server refresh tokens.
- /// </summary>
- /// <returns>A cryptographically strong symmetric key.</returns>
- private static byte[] CreateSecret() {
- // TODO: Replace this sample code with real code.
- // For this sample, we just generate random secrets.
- RandomNumberGenerator crypto = new RNGCryptoServiceProvider();
- var secret = new byte[16];
- crypto.GetBytes(secret);
- return secret;
- }
-
- /// <summary>
/// Creates the RSA key used by all the crypto service provider instances we create.
/// </summary>
/// <returns>RSA data that includes the private key.</returns>
diff --git a/samples/OAuthAuthorizationServer/Code/DataClasses.dbml b/samples/OAuthAuthorizationServer/Code/DataClasses.dbml
index 33e6eda..0ef987d 100644
--- a/samples/OAuthAuthorizationServer/Code/DataClasses.dbml
+++ b/samples/OAuthAuthorizationServer/Code/DataClasses.dbml
@@ -37,4 +37,12 @@
<Column Name="Timestamp" Type="System.DateTime" IsPrimaryKey="true" CanBeNull="false" />
</Type>
</Table>
+ <Table Name="" Member="SymmetricCryptoKeys">
+ <Type Name="SymmetricCryptoKey">
+ <Column Name="Bucket" Type="System.String" IsPrimaryKey="true" CanBeNull="false" UpdateCheck="Never" />
+ <Column Name="Handle" Type="System.String" IsPrimaryKey="true" CanBeNull="false" UpdateCheck="Never" />
+ <Column Name="ExpiresUtc" Type="System.DateTime" CanBeNull="false" UpdateCheck="Never" />
+ <Column Name="Secret" Type="System.Byte[]" CanBeNull="false" UpdateCheck="Never" />
+ </Type>
+ </Table>
</Database> \ No newline at end of file
diff --git a/samples/OAuthAuthorizationServer/Code/DataClasses.dbml.layout b/samples/OAuthAuthorizationServer/Code/DataClasses.dbml.layout
index e2982ce..f4de725 100644
--- a/samples/OAuthAuthorizationServer/Code/DataClasses.dbml.layout
+++ b/samples/OAuthAuthorizationServer/Code/DataClasses.dbml.layout
@@ -40,5 +40,11 @@
<classShapeMoniker Id="895ebbc8-8352-4c04-9e53-b8e6c8302d36" />
</nodes>
</associationConnector>
+ <classShape Id="93df6fa9-cc66-44a9-8885-960b1e670dd7" absoluteBounds="4.125, 6.25, 2, 1.5785953776041666">
+ <DataClassMoniker Name="/DataClassesDataContext/SymmetricCryptoKey" />
+ <nestedChildShapes>
+ <elementListCompartment Id="0b486eb8-31a4-4f11-b58f-09540c56319b" absoluteBounds="4.14, 6.71, 1.9700000000000002, 1.0185953776041665" name="DataPropertiesCompartment" titleTextColor="Black" itemTextColor="Black" />
+ </nestedChildShapes>
+ </classShape>
</nestedChildShapes>
</ordesignerObjectsDiagram> \ No newline at end of file
diff --git a/samples/OAuthAuthorizationServer/Code/DataClasses.designer.cs b/samples/OAuthAuthorizationServer/Code/DataClasses.designer.cs
index b6d070d..c8d1b19 100644
--- a/samples/OAuthAuthorizationServer/Code/DataClasses.designer.cs
+++ b/samples/OAuthAuthorizationServer/Code/DataClasses.designer.cs
@@ -2,7 +2,7 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
-// Runtime Version:4.0.30319.1
+// Runtime Version:4.0.30319.225
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@@ -42,6 +42,9 @@ namespace OAuthAuthorizationServer.Code
partial void InsertNonce(Nonce instance);
partial void UpdateNonce(Nonce instance);
partial void DeleteNonce(Nonce instance);
+ partial void InsertSymmetricCryptoKey(SymmetricCryptoKey instance);
+ partial void UpdateSymmetricCryptoKey(SymmetricCryptoKey instance);
+ partial void DeleteSymmetricCryptoKey(SymmetricCryptoKey instance);
#endregion
public DataClassesDataContext() :
@@ -105,6 +108,14 @@ namespace OAuthAuthorizationServer.Code
return this.GetTable<Nonce>();
}
}
+
+ public System.Data.Linq.Table<SymmetricCryptoKey> SymmetricCryptoKeys
+ {
+ get
+ {
+ return this.GetTable<SymmetricCryptoKey>();
+ }
+ }
}
[global::System.Data.Linq.Mapping.TableAttribute(Name="dbo.[User]")]
@@ -804,5 +815,139 @@ namespace OAuthAuthorizationServer.Code
}
}
}
+
+ [global::System.Data.Linq.Mapping.TableAttribute(Name="")]
+ public partial class SymmetricCryptoKey : INotifyPropertyChanging, INotifyPropertyChanged
+ {
+
+ private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty);
+
+ private string _Bucket;
+
+ private string _Handle;
+
+ private System.DateTime _ExpiresUtc;
+
+ private byte[] _Secret;
+
+ #region Extensibility Method Definitions
+ partial void OnLoaded();
+ partial void OnValidate(System.Data.Linq.ChangeAction action);
+ partial void OnCreated();
+ partial void OnBucketChanging(string value);
+ partial void OnBucketChanged();
+ partial void OnHandleChanging(string value);
+ partial void OnHandleChanged();
+ partial void OnExpiresUtcChanging(System.DateTime value);
+ partial void OnExpiresUtcChanged();
+ partial void OnSecretChanging(byte[] value);
+ partial void OnSecretChanged();
+ #endregion
+
+ public SymmetricCryptoKey()
+ {
+ OnCreated();
+ }
+
+ [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Bucket", CanBeNull=false, IsPrimaryKey=true, UpdateCheck=UpdateCheck.Never)]
+ public string Bucket
+ {
+ get
+ {
+ return this._Bucket;
+ }
+ set
+ {
+ if ((this._Bucket != value))
+ {
+ this.OnBucketChanging(value);
+ this.SendPropertyChanging();
+ this._Bucket = value;
+ this.SendPropertyChanged("Bucket");
+ this.OnBucketChanged();
+ }
+ }
+ }
+
+ [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Handle", CanBeNull=false, IsPrimaryKey=true, UpdateCheck=UpdateCheck.Never)]
+ public string Handle
+ {
+ get
+ {
+ return this._Handle;
+ }
+ set
+ {
+ if ((this._Handle != value))
+ {
+ this.OnHandleChanging(value);
+ this.SendPropertyChanging();
+ this._Handle = value;
+ this.SendPropertyChanged("Handle");
+ this.OnHandleChanged();
+ }
+ }
+ }
+
+ [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_ExpiresUtc", UpdateCheck=UpdateCheck.Never)]
+ public System.DateTime ExpiresUtc
+ {
+ get
+ {
+ return this._ExpiresUtc;
+ }
+ set
+ {
+ if ((this._ExpiresUtc != value))
+ {
+ this.OnExpiresUtcChanging(value);
+ this.SendPropertyChanging();
+ this._ExpiresUtc = value;
+ this.SendPropertyChanged("ExpiresUtc");
+ this.OnExpiresUtcChanged();
+ }
+ }
+ }
+
+ [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Secret", CanBeNull=false, UpdateCheck=UpdateCheck.Never)]
+ public byte[] Secret
+ {
+ get
+ {
+ return this._Secret;
+ }
+ set
+ {
+ if ((this._Secret != value))
+ {
+ this.OnSecretChanging(value);
+ this.SendPropertyChanging();
+ this._Secret = value;
+ this.SendPropertyChanged("Secret");
+ this.OnSecretChanged();
+ }
+ }
+ }
+
+ public event PropertyChangingEventHandler PropertyChanging;
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected virtual void SendPropertyChanging()
+ {
+ if ((this.PropertyChanging != null))
+ {
+ this.PropertyChanging(this, emptyChangingEventArgs);
+ }
+ }
+
+ protected virtual void SendPropertyChanged(String propertyName)
+ {
+ if ((this.PropertyChanged != null))
+ {
+ this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+ }
}
#pragma warning restore 1591
diff --git a/samples/OAuthAuthorizationServer/Code/DatabaseNonceStore.cs b/samples/OAuthAuthorizationServer/Code/DatabaseKeyNonceStore.cs
index a0ce19e..765696e 100644
--- a/samples/OAuthAuthorizationServer/Code/DatabaseNonceStore.cs
+++ b/samples/OAuthAuthorizationServer/Code/DatabaseKeyNonceStore.cs
@@ -1,16 +1,18 @@
namespace OAuthAuthorizationServer.Code {
using System;
+ using System.Collections.Generic;
using System.Data.SqlClient;
+ using System.Linq;
using DotNetOpenAuth.Messaging.Bindings;
/// <summary>
/// A database-persisted nonce store.
/// </summary>
- public class DatabaseNonceStore : INonceStore {
+ public class DatabaseKeyNonceStore : INonceStore, ICryptoKeyStore {
/// <summary>
- /// Initializes a new instance of the <see cref="DatabaseNonceStore"/> class.
+ /// Initializes a new instance of the <see cref="DatabaseKeyNonceStore"/> class.
/// </summary>
- public DatabaseNonceStore() {
+ public DatabaseKeyNonceStore() {
}
#region INonceStore Members
@@ -51,5 +53,43 @@
}
#endregion
+
+ #region ICryptoKeyStore Members
+
+ public CryptoKey GetKey(string bucket, string handle) {
+ // It is critical that this lookup be case-sensitive, which can only be configured at the database.
+ var matches = from key in MvcApplication.DataContext.SymmetricCryptoKeys
+ where key.Bucket == bucket && key.Handle == handle
+ select new CryptoKey(key.Secret, key.ExpiresUtc.AsUtc());
+
+ return matches.FirstOrDefault();
+ }
+
+ public IEnumerable<KeyValuePair<string, CryptoKey>> GetKeys(string bucket) {
+ return from key in MvcApplication.DataContext.SymmetricCryptoKeys
+ orderby key.ExpiresUtc descending
+ select new KeyValuePair<string, CryptoKey>(key.Handle, new CryptoKey(key.Secret, key.ExpiresUtc.AsUtc()));
+ }
+
+ public void StoreKey(string bucket, string handle, CryptoKey key) {
+ var keyRow = new SymmetricCryptoKey() {
+ Bucket = bucket,
+ Handle = handle,
+ Secret = key.Key,
+ ExpiresUtc = key.ExpiresUtc,
+ };
+
+ MvcApplication.DataContext.SymmetricCryptoKeys.InsertOnSubmit(keyRow);
+ MvcApplication.DataContext.SubmitChanges();
+ }
+
+ public void RemoveKey(string bucket, string handle) {
+ var match = MvcApplication.DataContext.SymmetricCryptoKeys.FirstOrDefault(k => k.Bucket == bucket && k.Handle == handle);
+ if (match != null) {
+ MvcApplication.DataContext.SymmetricCryptoKeys.DeleteOnSubmit(match);
+ }
+ }
+
+ #endregion
}
} \ No newline at end of file
diff --git a/samples/OAuthAuthorizationServer/Code/OAuth2AuthorizationServer.cs b/samples/OAuthAuthorizationServer/Code/OAuth2AuthorizationServer.cs
index 3be70f0..90f99f8 100644
--- a/samples/OAuthAuthorizationServer/Code/OAuth2AuthorizationServer.cs
+++ b/samples/OAuthAuthorizationServer/Code/OAuth2AuthorizationServer.cs
@@ -13,18 +13,14 @@
internal class OAuth2AuthorizationServer : IAuthorizationServer {
private static readonly RSAParameters AsymmetricTokenSigningPrivateKey = CreateRSAKey();
- private static readonly byte[] secret = CreateSecret();
-
- private readonly INonceStore nonceStore = new DatabaseNonceStore();
-
#region Implementation of IAuthorizationServer
- public byte[] Secret {
- get { return secret; }
+ public ICryptoKeyStore CryptoKeyStore {
+ get { return MvcApplication.KeyNonceStore; }
}
public INonceStore VerificationCodeNonceStore {
- get { return this.nonceStore; }
+ get { return MvcApplication.KeyNonceStore; }
}
public RSACryptoServiceProvider CreateAccessTokenSigningCryptoServiceProvider() {
@@ -75,19 +71,6 @@
}
/// <summary>
- /// Creates a symmetric secret used to sign and encrypt authorization server refresh tokens.
- /// </summary>
- /// <returns>A cryptographically strong symmetric key.</returns>
- private static byte[] CreateSecret() {
- // TODO: Replace this sample code with real code.
- // For this sample, we just generate random secrets.
- RandomNumberGenerator crypto = new RNGCryptoServiceProvider();
- var secret = new byte[32]; // 256-bit symmetric key to protect all protected resources.
- crypto.GetBytes(secret);
- return secret;
- }
-
- /// <summary>
/// Creates the RSA key used by all the crypto service provider instances we create.
/// </summary>
/// <returns>RSA data that includes the private key.</returns>
@@ -126,12 +109,12 @@
private bool IsAuthorizationValid(HashSet<string> requestedScopes, string clientIdentifier, DateTime issuedUtc, string username) {
var grantedScopeStrings = from auth in MvcApplication.DataContext.ClientAuthorizations
- where
- auth.Client.ClientIdentifier == clientIdentifier &&
- auth.CreatedOnUtc <= issuedUtc &&
- (!auth.ExpirationDateUtc.HasValue || auth.ExpirationDateUtc.Value >= DateTime.UtcNow) &&
- auth.User.OpenIDClaimedIdentifier == username
- select auth.Scope;
+ where
+ auth.Client.ClientIdentifier == clientIdentifier &&
+ auth.CreatedOnUtc <= issuedUtc &&
+ (!auth.ExpirationDateUtc.HasValue || auth.ExpirationDateUtc.Value >= DateTime.UtcNow) &&
+ auth.User.OpenIDClaimedIdentifier == username
+ select auth.Scope;
if (!grantedScopeStrings.Any()) {
// No granted authorizations prior to the issuance of this token, so it must have been revoked.
diff --git a/samples/OAuthAuthorizationServer/Code/Utilities.cs b/samples/OAuthAuthorizationServer/Code/Utilities.cs
new file mode 100644
index 0000000..c9109bd
--- /dev/null
+++ b/samples/OAuthAuthorizationServer/Code/Utilities.cs
@@ -0,0 +1,21 @@
+namespace OAuthAuthorizationServer.Code {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Web;
+
+ internal static class Utilities {
+ /// <summary>
+ /// Ensures that local times are converted to UTC times. Unspecified kinds are recast to UTC with no conversion.
+ /// </summary>
+ /// <param name="value">The date-time to convert.</param>
+ /// <returns>The date-time in UTC time.</returns>
+ internal static DateTime AsUtc(this DateTime value) {
+ if (value.Kind == DateTimeKind.Unspecified) {
+ return new DateTime(value.Ticks, DateTimeKind.Utc);
+ }
+
+ return value.ToUniversalTime();
+ }
+ }
+} \ No newline at end of file
diff --git a/samples/OAuthAuthorizationServer/Controllers/OAuthController.cs b/samples/OAuthAuthorizationServer/Controllers/OAuthController.cs
index 47c1977..11e7b11 100644
--- a/samples/OAuthAuthorizationServer/Controllers/OAuthController.cs
+++ b/samples/OAuthAuthorizationServer/Controllers/OAuthController.cs
@@ -35,16 +35,6 @@
#endif
/// <summary>
- /// Creates the resource server's encryption service provider with private key.
- /// </summary>
- /// <returns>An RSA crypto service provider.</returns>
- internal static RSACryptoServiceProvider CreateResourceServerEncryptionServiceProvider() {
- var resourceServerEncryptionServiceProvider = new RSACryptoServiceProvider();
- resourceServerEncryptionServiceProvider.ImportParameters(ResourceServerEncryptionPublicKey);
- return resourceServerEncryptionServiceProvider;
- }
-
- /// <summary>
/// The OAuth 2.0 token endpoint.
/// </summary>
/// <returns>The response to the Client.</returns>
@@ -133,5 +123,15 @@
return this.authorizationServer.Channel.PrepareResponse(response).AsActionResult();
}
+
+ /// <summary>
+ /// Creates the resource server's encryption service provider with private key.
+ /// </summary>
+ /// <returns>An RSA crypto service provider.</returns>
+ internal static RSACryptoServiceProvider CreateResourceServerEncryptionServiceProvider() {
+ var resourceServerEncryptionServiceProvider = new RSACryptoServiceProvider();
+ resourceServerEncryptionServiceProvider.ImportParameters(ResourceServerEncryptionPublicKey);
+ return resourceServerEncryptionServiceProvider;
+ }
}
}
diff --git a/samples/OAuthAuthorizationServer/Global.asax.cs b/samples/OAuthAuthorizationServer/Global.asax.cs
index 2c23ec0..d878ea6 100644
--- a/samples/OAuthAuthorizationServer/Global.asax.cs
+++ b/samples/OAuthAuthorizationServer/Global.asax.cs
@@ -26,7 +26,7 @@
/// </summary>
public static log4net.ILog Logger = log4net.LogManager.GetLogger("DotNetOpenAuth.OAuthAuthorizationServer");
- public static DatabaseNonceStore NonceStore { get; set; }
+ public static DatabaseKeyNonceStore KeyNonceStore { get; set; }
/// <summary>
/// Gets the transaction-protected database connection for the current request.
@@ -81,7 +81,7 @@
RegisterRoutes(RouteTable.Routes);
- NonceStore = new DatabaseNonceStore();
+ KeyNonceStore = new DatabaseKeyNonceStore();
log4net.Config.XmlConfigurator.Configure();
Logger.Info("Sample starting...");
diff --git a/samples/OAuthAuthorizationServer/OAuthAuthorizationServer.csproj b/samples/OAuthAuthorizationServer/OAuthAuthorizationServer.csproj
index 5bb1daf..d00cef4 100644
--- a/samples/OAuthAuthorizationServer/OAuthAuthorizationServer.csproj
+++ b/samples/OAuthAuthorizationServer/OAuthAuthorizationServer.csproj
@@ -70,13 +70,14 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Code\Client.cs" />
- <Compile Include="Code\DatabaseNonceStore.cs" />
+ <Compile Include="Code\DatabaseKeyNonceStore.cs" />
<Compile Include="Code\DataClasses.designer.cs">
<DependentUpon>DataClasses.dbml</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
<Compile Include="Code\OAuth2AuthorizationServer.cs" />
+ <Compile Include="Code\Utilities.cs" />
<Compile Include="Controllers\AccountController.cs" />
<Compile Include="Controllers\HomeController.cs" />
<Compile Include="Controllers\OAuthController.cs" />
diff --git a/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd b/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd
index 8b6d6c1..065b5ee 100644
--- a/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd
+++ b/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd
@@ -236,7 +236,7 @@
</xs:documentation>
</xs:annotation>
</xs:attribute>
- <xs:attribute name="privateSecretMaximumAge" type="xs:string">
+ <xs:attribute name="privateSecretMaximumAge" type="xs:string" default="28.00:00:00">
<xs:annotation>
<xs:documentation>
The maximum age of a secret used for private signing or encryption before it is renewed.
diff --git a/src/DotNetOpenAuth/Configuration/MessagingElement.cs b/src/DotNetOpenAuth/Configuration/MessagingElement.cs
index d85f799..1c46bcf 100644
--- a/src/DotNetOpenAuth/Configuration/MessagingElement.cs
+++ b/src/DotNetOpenAuth/Configuration/MessagingElement.cs
@@ -73,8 +73,8 @@ namespace DotNetOpenAuth.Configuration {
/// Gets or sets the maximum lifetime of a private symmetric secret,
/// that may be used for signing or encryption.
/// </summary>
- /// <value>The default value is 7 days.</value>
- [ConfigurationProperty(PrivateSecretMaximumAgeConfigName, DefaultValue = "07:00:00")]
+ /// <value>The default value is 28 days (twice the age of the longest association).</value>
+ [ConfigurationProperty(PrivateSecretMaximumAgeConfigName, DefaultValue = "28.00:00:00")]
public TimeSpan PrivateSecretMaximumAge {
get { return (TimeSpan)this[PrivateSecretMaximumAgeConfigName]; }
set { this[PrivateSecretMaximumAgeConfigName] = value; }
diff --git a/src/DotNetOpenAuth/Configuration/OpenIdProviderSecuritySettingsElement.cs b/src/DotNetOpenAuth/Configuration/OpenIdProviderSecuritySettingsElement.cs
index 8a922f7..0d8e8b4 100644
--- a/src/DotNetOpenAuth/Configuration/OpenIdProviderSecuritySettingsElement.cs
+++ b/src/DotNetOpenAuth/Configuration/OpenIdProviderSecuritySettingsElement.cs
@@ -35,6 +35,9 @@ namespace DotNetOpenAuth.Configuration {
/// </summary>
private const string AssociationsConfigName = "associations";
+ /// <summary>
+ /// The name of the @encodeAssociationSecretsInHandles attribute.
+ /// </summary>
private const string EncodeAssociationSecretsInHandlesConfigName = "encodeAssociationSecretsInHandles";
/// <summary>
diff --git a/src/DotNetOpenAuth/Messaging/BinaryDataBagFormatter.cs b/src/DotNetOpenAuth/Messaging/BinaryDataBagFormatter.cs
index 08c1219..d44d9bb 100644
--- a/src/DotNetOpenAuth/Messaging/BinaryDataBagFormatter.cs
+++ b/src/DotNetOpenAuth/Messaging/BinaryDataBagFormatter.cs
@@ -34,15 +34,17 @@ namespace DotNetOpenAuth.Messaging {
/// <summary>
/// Initializes a new instance of the <see cref="UriStyleMessageFormatter&lt;T&gt;"/> class.
/// </summary>
- /// <param name="symmetricSecret">The symmetric secret to use for signing and encrypting.</param>
+ /// <param name="cryptoKeyStore">The crypto key store used when signing or encrypting.</param>
+ /// <param name="bucket">The bucket in which symmetric keys are stored for signing/encrypting data.</param>
/// <param name="signed">A value indicating whether the data in this instance will be protected against tampering.</param>
/// <param name="encrypted">A value indicating whether the data in this instance will be protected against eavesdropping.</param>
/// <param name="compressed">A value indicating whether the data in this instance will be GZip'd.</param>
+ /// <param name="minimumAge">The minimum age.</param>
/// <param name="maximumAge">The maximum age of a token that can be decoded; useful only when <paramref name="decodeOnceOnly"/> is <c>true</c>.</param>
/// <param name="decodeOnceOnly">The nonce store to use to ensure that this instance is only decoded once.</param>
- protected internal BinaryDataBagFormatter(byte[] symmetricSecret = null, bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null)
- : base(symmetricSecret, signed, encrypted, compressed, maximumAge, decodeOnceOnly) {
- Contract.Requires<ArgumentException>(symmetricSecret != null || (!signed && !encrypted), "A secret is required when signing or encrypting is required.");
+ protected internal BinaryDataBagFormatter(ICryptoKeyStore cryptoKeyStore = null, string bucket = null, bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? minimumAge = null, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null)
+ : base(cryptoKeyStore, bucket, signed, encrypted, compressed, minimumAge, maximumAge, decodeOnceOnly) {
+ Contract.Requires<ArgumentException>((cryptoKeyStore != null && bucket != null) || (!signed && !encrypted), "A secret is required when signing or encrypting is required.");
}
/// <summary>
diff --git a/src/DotNetOpenAuth/Messaging/Bindings/CryptoKeyCollisionException.cs b/src/DotNetOpenAuth/Messaging/Bindings/CryptoKeyCollisionException.cs
index 167f7b0..e7dbf46 100644
--- a/src/DotNetOpenAuth/Messaging/Bindings/CryptoKeyCollisionException.cs
+++ b/src/DotNetOpenAuth/Messaging/Bindings/CryptoKeyCollisionException.cs
@@ -8,6 +8,10 @@ namespace DotNetOpenAuth.Messaging.Bindings {
using System;
using System.Security.Permissions;
+ /// <summary>
+ /// Thrown by a hosting application or web site when a cryptographic key is created with a
+ /// bucket and handle that conflicts with a previously stored and unexpired key.
+ /// </summary>
[Serializable]
public class CryptoKeyCollisionException : ArgumentException {
/// <summary>
diff --git a/src/DotNetOpenAuth/Messaging/DataBagFormatterBase.cs b/src/DotNetOpenAuth/Messaging/DataBagFormatterBase.cs
index 98f5e8c..b10a36f 100644
--- a/src/DotNetOpenAuth/Messaging/DataBagFormatterBase.cs
+++ b/src/DotNetOpenAuth/Messaging/DataBagFormatterBase.cs
@@ -33,14 +33,19 @@ namespace DotNetOpenAuth.Messaging {
private const int NonceLength = 6;
/// <summary>
- /// The symmetric secret used for signing/encryption of verification codes and refresh tokens.
+ /// The minimum allowable lifetime for the key used to encrypt/decrypt or sign this databag.
/// </summary>
- private readonly byte[] symmetricSecret;
+ private readonly TimeSpan minimumAge = TimeSpan.FromDays(1);
/// <summary>
- /// The hashing algorithm to use while signing when using a symmetric secret.
+ /// The symmetric key store with the secret used for signing/encryption of verification codes and refresh tokens.
/// </summary>
- private readonly HashAlgorithm symmetricHasher;
+ private readonly ICryptoKeyStore cryptoKeyStore;
+
+ /// <summary>
+ /// The bucket for symmetric keys.
+ /// </summary>
+ private readonly string cryptoKeyBucket;
/// <summary>
/// The crypto to use for signing access tokens.
@@ -100,21 +105,24 @@ namespace DotNetOpenAuth.Messaging {
/// <summary>
/// Initializes a new instance of the <see cref="UriStyleMessageFormatter&lt;T&gt;"/> class.
/// </summary>
- /// <param name="symmetricSecret">The symmetric secret to use for signing and encrypting.</param>
+ /// <param name="cryptoKeyStore">The crypto key store used when signing or encrypting.</param>
+ /// <param name="bucket">The bucket in which symmetric keys are stored for signing/encrypting data.</param>
/// <param name="signed">A value indicating whether the data in this instance will be protected against tampering.</param>
/// <param name="encrypted">A value indicating whether the data in this instance will be protected against eavesdropping.</param>
/// <param name="compressed">A value indicating whether the data in this instance will be GZip'd.</param>
+ /// <param name="minimumAge">The required minimum lifespan within which this token must be decodable and verifiable; useful only when <paramref name="signed"/> and/or <paramref name="encrypted"/> is true.</param>
/// <param name="maximumAge">The maximum age of a token that can be decoded; useful only when <paramref name="decodeOnceOnly"/> is <c>true</c>.</param>
/// <param name="decodeOnceOnly">The nonce store to use to ensure that this instance is only decoded once.</param>
- protected DataBagFormatterBase(byte[] symmetricSecret = null, bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null)
+ protected DataBagFormatterBase(ICryptoKeyStore cryptoKeyStore = null, string bucket = null, bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? minimumAge = null, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null)
: this(signed, encrypted, compressed, maximumAge, decodeOnceOnly) {
- Contract.Requires<ArgumentException>(symmetricSecret != null || (!signed && !encrypted), "A secret is required when signing or encrypting is required.");
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(bucket) || cryptoKeyStore == null);
+ Contract.Requires<ArgumentException>(cryptoKeyStore != null || (!signed && !encrypted), "A secret is required when signing or encrypting is required.");
- if (symmetricSecret != null) {
- this.symmetricHasher = new HMACSHA256(symmetricSecret);
+ this.cryptoKeyStore = cryptoKeyStore;
+ this.cryptoKeyBucket = bucket;
+ if (minimumAge.HasValue) {
+ this.minimumAge = minimumAge.Value;
}
-
- this.symmetricSecret = symmetricSecret;
}
/// <summary>
@@ -154,12 +162,13 @@ namespace DotNetOpenAuth.Messaging {
encoded = MessagingUtilities.Compress(encoded);
}
+ string symmetricSecretHandle = null;
if (this.encrypted) {
- encoded = this.Encrypt(encoded);
+ encoded = this.Encrypt(encoded, out symmetricSecretHandle);
}
if (this.signed) {
- message.Signature = this.CalculateSignature(encoded);
+ message.Signature = this.CalculateSignature(encoded, symmetricSecretHandle);
}
int capacity = this.signed ? 4 + message.Signature.Length + 4 + encoded.Length : encoded.Length;
@@ -172,7 +181,13 @@ namespace DotNetOpenAuth.Messaging {
writer.WriteBuffer(encoded);
writer.Flush();
- return Convert.ToBase64String(finalStream.ToArray());
+ string payload = MessagingUtilities.ConvertToBase64WebSafeString(finalStream.ToArray());
+ string result = payload;
+ if (symmetricSecretHandle != null && (this.signed || this.encrypted)) {
+ result = MessagingUtilities.CombineKeyHandleAndPayload(symmetricSecretHandle, payload);
+ }
+
+ return result;
}
/// <summary>
@@ -182,8 +197,15 @@ namespace DotNetOpenAuth.Messaging {
/// <param name="value">The serialized form of the <see cref="DataBag"/> to deserialize. Must not be null or empty.</param>
/// <returns>The deserialized value. Never null.</returns>
public T Deserialize(IProtocolMessage containingMessage, string value) {
+ string symmetricSecretHandle = null;
+ if (this.encrypted && this.cryptoKeyStore != null) {
+ string valueWithoutHandle;
+ MessagingUtilities.ExtractKeyHandleAndPayload(containingMessage, "<TODO>", value, out symmetricSecretHandle, out valueWithoutHandle);
+ value = valueWithoutHandle;
+ }
+
var message = new T { ContainingMessage = containingMessage };
- byte[] data = Convert.FromBase64String(value);
+ byte[] data = MessagingUtilities.FromBase64WebSafeString(value);
byte[] signature = null;
if (this.signed) {
@@ -193,11 +215,11 @@ namespace DotNetOpenAuth.Messaging {
data = dataReader.ReadBuffer();
// Verify that the verification code was issued by message authorization server.
- ErrorUtilities.VerifyProtocol(this.IsSignatureValid(data, signature), MessagingStrings.SignatureInvalid);
+ ErrorUtilities.VerifyProtocol(this.IsSignatureValid(data, signature, symmetricSecretHandle), MessagingStrings.SignatureInvalid);
}
if (this.encrypted) {
- data = this.Decrypt(data);
+ data = this.Decrypt(data, symmetricSecretHandle);
}
if (this.compressed) {
@@ -249,17 +271,18 @@ namespace DotNetOpenAuth.Messaging {
/// </summary>
/// <param name="signedData">The signed data.</param>
/// <param name="signature">The signature.</param>
+ /// <param name="symmetricSecretHandle">The symmetric secret handle. <c>null</c> when using an asymmetric algorithm.</param>
/// <returns>
/// <c>true</c> if the signature is valid; otherwise, <c>false</c>.
/// </returns>
- private bool IsSignatureValid(byte[] signedData, byte[] signature) {
+ private bool IsSignatureValid(byte[] signedData, byte[] signature, string symmetricSecretHandle) {
Contract.Requires<ArgumentNullException>(signedData != null, "message");
Contract.Requires<ArgumentNullException>(signature != null, "signature");
if (this.asymmetricSigning != null) {
return this.asymmetricSigning.VerifyData(signedData, this.hasherForAsymmetricSigning, signature);
} else {
- return MessagingUtilities.AreEquivalentConstantTime(signature, this.CalculateSignature(signedData));
+ return MessagingUtilities.AreEquivalentConstantTime(signature, this.CalculateSignature(signedData, symmetricSecretHandle));
}
}
@@ -267,18 +290,22 @@ namespace DotNetOpenAuth.Messaging {
/// Calculates the signature for the data in this verification code.
/// </summary>
/// <param name="bytesToSign">The bytes to sign.</param>
+ /// <param name="symmetricSecretHandle">The symmetric secret handle. <c>null</c> when using an asymmetric algorithm.</param>
/// <returns>
/// The calculated signature.
/// </returns>
- private byte[] CalculateSignature(byte[] bytesToSign) {
+ private byte[] CalculateSignature(byte[] bytesToSign, string symmetricSecretHandle) {
Contract.Requires<ArgumentNullException>(bytesToSign != null, "bytesToSign");
- Contract.Requires<InvalidOperationException>(this.asymmetricSigning != null || this.symmetricHasher != null);
+ Contract.Requires<InvalidOperationException>(this.asymmetricSigning != null || this.cryptoKeyStore != null);
Contract.Ensures(Contract.Result<byte[]>() != null);
if (this.asymmetricSigning != null) {
return this.asymmetricSigning.SignData(bytesToSign, this.hasherForAsymmetricSigning);
} else {
- return this.symmetricHasher.ComputeHash(bytesToSign);
+ var key = this.cryptoKeyStore.GetKey(this.cryptoKeyBucket, symmetricSecretHandle);
+ ErrorUtilities.VerifyProtocol(key != null, "Missing decryption key.");
+ var symmetricHasher = new HMACSHA256(key.Key);
+ return symmetricHasher.ComputeHash(bytesToSign);
}
}
@@ -286,14 +313,20 @@ namespace DotNetOpenAuth.Messaging {
/// Encrypts the specified value using either the symmetric or asymmetric encryption algorithm as appropriate.
/// </summary>
/// <param name="value">The value.</param>
- /// <returns>The encrypted value.</returns>
- private byte[] Encrypt(byte[] value) {
- Contract.Requires<InvalidOperationException>(this.asymmetricEncrypting != null || this.symmetricSecret != null);
+ /// <param name="symmetricSecretHandle">Receives the symmetric secret handle. <c>null</c> when using an asymmetric algorithm.</param>
+ /// <returns>
+ /// The encrypted value.
+ /// </returns>
+ private byte[] Encrypt(byte[] value, out string symmetricSecretHandle) {
+ Contract.Requires<InvalidOperationException>(this.asymmetricEncrypting != null || this.cryptoKeyStore != null);
if (this.asymmetricEncrypting != null) {
+ symmetricSecretHandle = null;
return this.asymmetricEncrypting.EncryptWithRandomSymmetricKey(value);
} else {
- return MessagingUtilities.Encrypt(value, this.symmetricSecret);
+ var cryptoKey = this.cryptoKeyStore.GetCurrentKey(this.cryptoKeyBucket, this.minimumAge);
+ symmetricSecretHandle = cryptoKey.Key;
+ return MessagingUtilities.Encrypt(value, cryptoKey.Value.Key);
}
}
@@ -301,14 +334,19 @@ namespace DotNetOpenAuth.Messaging {
/// Decrypts the specified value using either the symmetric or asymmetric encryption algorithm as appropriate.
/// </summary>
/// <param name="value">The value.</param>
- /// <returns>The decrypted value.</returns>
- private byte[] Decrypt(byte[] value) {
- Contract.Requires<InvalidOperationException>(this.asymmetricEncrypting != null || this.symmetricSecret != null);
+ /// <param name="symmetricSecretHandle">The symmetric secret handle. <c>null</c> when using an asymmetric algorithm.</param>
+ /// <returns>
+ /// The decrypted value.
+ /// </returns>
+ private byte[] Decrypt(byte[] value, string symmetricSecretHandle) {
+ Contract.Requires<InvalidOperationException>(this.asymmetricEncrypting != null || symmetricSecretHandle != null);
if (this.asymmetricEncrypting != null) {
return this.asymmetricEncrypting.DecryptWithRandomSymmetricKey(value);
} else {
- return MessagingUtilities.Decrypt(value, this.symmetricSecret);
+ var key = this.cryptoKeyStore.GetKey(this.cryptoKeyBucket, symmetricSecretHandle);
+ ErrorUtilities.VerifyProtocol(key != null, "Missing decryption key.");
+ return MessagingUtilities.Decrypt(value, key.Key);
}
}
}
diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs
index b1807c9..8686c2e 100644
--- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs
+++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs
@@ -29,20 +29,6 @@ namespace DotNetOpenAuth.Messaging {
/// </summary>
public static class MessagingUtilities {
/// <summary>
- /// The length of private symmetric secret handles.
- /// </summary>
- /// <remarks>
- /// This value needn't be high, as we only expect to have a small handful of unexpired secrets at a time,
- /// and handle recycling is permissible.
- /// </remarks>
- private const int SymmetricSecretHandleLength = 4;
-
- /// <summary>
- /// The default lifetime of a private secret.
- /// </summary>
- private static readonly TimeSpan SymmetricSecretKeyLifespan = Configuration.DotNetOpenAuthSection.Configuration.Messaging.PrivateSecretMaximumAge;
-
- /// <summary>
/// The cryptographically strong random data generator used for creating secrets.
/// </summary>
/// <remarks>The random number generator is thread-safe.</remarks>
@@ -91,6 +77,20 @@ namespace DotNetOpenAuth.Messaging {
internal const string AlphaNumericNoLookAlikes = "23456789abcdefghjkmnpqrstwxyzABCDEFGHJKMNPQRSTWXYZ";
/// <summary>
+ /// The length of private symmetric secret handles.
+ /// </summary>
+ /// <remarks>
+ /// This value needn't be high, as we only expect to have a small handful of unexpired secrets at a time,
+ /// and handle recycling is permissible.
+ /// </remarks>
+ private const int SymmetricSecretHandleLength = 4;
+
+ /// <summary>
+ /// The default lifetime of a private secret.
+ /// </summary>
+ private static readonly TimeSpan SymmetricSecretKeyLifespan = Configuration.DotNetOpenAuthSection.Configuration.Messaging.PrivateSecretMaximumAge;
+
+ /// <summary>
/// A character array containing just the = character.
/// </summary>
private static readonly char[] EqualsArray = new char[] { '=' };
@@ -427,6 +427,40 @@ namespace DotNetOpenAuth.Messaging {
}
/// <summary>
+ /// Encodes a symmetric key handle and the blob that is encrypted/signed with that key into a single string
+ /// that can be decoded by <see cref="ExtractKeyHandleAndPayload"/>.
+ /// </summary>
+ /// <param name="handle">The cryptographic key handle.</param>
+ /// <param name="payload">The encrypted/signed blob.</param>
+ /// <returns>The combined encoded value.</returns>
+ internal static string CombineKeyHandleAndPayload(string handle, string payload) {
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(handle));
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(payload));
+ Contract.Ensures(!String.IsNullOrEmpty(Contract.Result<string>()));
+
+ return handle + "!" + payload;
+ }
+
+ /// <summary>
+ /// Extracts the key handle and encrypted blob from a string previously returned from <see cref="CombineKeyHandleAndPayload"/>.
+ /// </summary>
+ /// <param name="containingMessage">The containing message.</param>
+ /// <param name="messagePart">The message part.</param>
+ /// <param name="keyHandleAndBlob">The value previously returned from <see cref="CombineKeyHandleAndPayload"/>.</param>
+ /// <param name="handle">The crypto key handle.</param>
+ /// <param name="dataBlob">The encrypted/signed data.</param>
+ internal static void ExtractKeyHandleAndPayload(IProtocolMessage containingMessage, string messagePart, string keyHandleAndBlob, out string handle, out string dataBlob) {
+ Contract.Requires<ArgumentNullException>(containingMessage != null, "containingMessage");
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(messagePart));
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(keyHandleAndBlob));
+
+ int privateHandleIndex = keyHandleAndBlob.IndexOf('!');
+ ErrorUtilities.VerifyProtocol(privateHandleIndex > 0, MessagingStrings.UnexpectedMessagePartValue, messagePart, keyHandleAndBlob);
+ handle = keyHandleAndBlob.Substring(0, privateHandleIndex);
+ dataBlob = keyHandleAndBlob.Substring(privateHandleIndex + 1);
+ }
+
+ /// <summary>
/// Gets a buffer of random data (not cryptographically strong).
/// </summary>
/// <param name="length">The length of the sequence to generate.</param>
@@ -779,8 +813,20 @@ namespace DotNetOpenAuth.Messaging {
Contract.Ensures(Contract.Result<byte[]>() != null);
// Restore the padding characters and original URL-unsafe characters.
- int missingPaddingCharacters = 4 - (base64WebSafe.Length % 4);
- ErrorUtilities.VerifyInternal(missingPaddingCharacters <= 2, "No more than two padding characters should be present for base64.");
+ int missingPaddingCharacters;
+ switch (base64WebSafe.Length % 4) {
+ case 3:
+ missingPaddingCharacters = 1;
+ break;
+ case 2:
+ missingPaddingCharacters = 2;
+ break;
+ case 0:
+ missingPaddingCharacters = 0;
+ break;
+ default:
+ throw ErrorUtilities.ThrowInternal("No more than two padding characters should be present for base64.");
+ }
var builder = new StringBuilder(base64WebSafe, base64WebSafe.Length + missingPaddingCharacters);
builder.Replace('-', '+').Replace('_', '/');
builder.Append('=', missingPaddingCharacters);
diff --git a/src/DotNetOpenAuth/Messaging/UriStyleMessageFormatter.cs b/src/DotNetOpenAuth/Messaging/UriStyleMessageFormatter.cs
index b435f1b..8c66128 100644
--- a/src/DotNetOpenAuth/Messaging/UriStyleMessageFormatter.cs
+++ b/src/DotNetOpenAuth/Messaging/UriStyleMessageFormatter.cs
@@ -36,15 +36,17 @@ namespace DotNetOpenAuth.Messaging {
/// <summary>
/// Initializes a new instance of the <see cref="UriStyleMessageFormatter&lt;T&gt;"/> class.
/// </summary>
- /// <param name="symmetricSecret">The symmetric secret to use for signing and encrypting.</param>
+ /// <param name="cryptoKeyStore">The crypto key store used when signing or encrypting.</param>
+ /// <param name="bucket">The bucket in which symmetric keys are stored for signing/encrypting data.</param>
/// <param name="signed">A value indicating whether the data in this instance will be protected against tampering.</param>
/// <param name="encrypted">A value indicating whether the data in this instance will be protected against eavesdropping.</param>
/// <param name="compressed">A value indicating whether the data in this instance will be GZip'd.</param>
+ /// <param name="minimumAge">The minimum age.</param>
/// <param name="maximumAge">The maximum age of a token that can be decoded; useful only when <paramref name="decodeOnceOnly"/> is <c>true</c>.</param>
/// <param name="decodeOnceOnly">The nonce store to use to ensure that this instance is only decoded once.</param>
- protected internal UriStyleMessageFormatter(byte[] symmetricSecret = null, bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null)
- : base(symmetricSecret, signed, encrypted, compressed, maximumAge, decodeOnceOnly) {
- Contract.Requires<ArgumentException>(symmetricSecret != null || (!signed && !encrypted), "A secret is required when signing or encrypting is required.");
+ protected internal UriStyleMessageFormatter(ICryptoKeyStore cryptoKeyStore = null, string bucket = null, bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? minimumAge = null, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null)
+ : base(cryptoKeyStore, bucket, signed, encrypted, compressed, minimumAge, maximumAge, decodeOnceOnly) {
+ Contract.Requires<ArgumentException>((cryptoKeyStore != null && !String.IsNullOrEmpty(bucket)) || (!signed && !encrypted), "A secret is required when signing or encrypting is required.");
}
/// <summary>
diff --git a/src/DotNetOpenAuth/OAuth2/AuthorizationServer.cs b/src/DotNetOpenAuth/OAuth2/AuthorizationServer.cs
index a48c95a..82334ef 100644
--- a/src/DotNetOpenAuth/OAuth2/AuthorizationServer.cs
+++ b/src/DotNetOpenAuth/OAuth2/AuthorizationServer.cs
@@ -233,7 +233,7 @@ namespace DotNetOpenAuth.OAuth2 {
response.Scope.ResetContents(tokenRequest.AuthorizationDescription.Scope);
if (includeRefreshToken) {
- var refreshTokenFormatter = RefreshToken.CreateFormatter(this.AuthorizationServerServices.Secret);
+ var refreshTokenFormatter = RefreshToken.CreateFormatter(this.AuthorizationServerServices.CryptoKeyStore);
var refreshToken = new RefreshToken(tokenRequest.AuthorizationDescription);
response.RefreshToken = refreshTokenFormatter.Serialize(refreshToken);
}
diff --git a/src/DotNetOpenAuth/OAuth2/ChannelElements/AccessRequestBindingElement.cs b/src/DotNetOpenAuth/OAuth2/ChannelElements/AccessRequestBindingElement.cs
index 2404963..b772c0e 100644
--- a/src/DotNetOpenAuth/OAuth2/ChannelElements/AccessRequestBindingElement.cs
+++ b/src/DotNetOpenAuth/OAuth2/ChannelElements/AccessRequestBindingElement.cs
@@ -55,8 +55,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements {
public override MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) {
var response = message as ITokenCarryingRequest;
if (response != null) {
- switch (response.CodeOrTokenType)
- {
+ switch (response.CodeOrTokenType) {
case CodeOrTokenType.AuthorizationCode:
var codeFormatter = AuthorizationCode.CreateFormatter(this.AuthorizationServer);
var code = (AuthorizationCode)response.AuthorizationDescription;
@@ -70,8 +69,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements {
}
var accessTokenResponse = message as AccessTokenSuccessResponse;
- if (accessTokenResponse != null)
- {
+ if (accessTokenResponse != null) {
var directResponseMessage = (IDirectResponseProtocolMessage)accessTokenResponse;
var accessTokenRequest = (AccessTokenRequestBase)directResponseMessage.OriginatingRequest;
ErrorUtilities.VerifyProtocol(accessTokenRequest.GrantType != GrantType.None || accessTokenResponse.RefreshToken == null, OAuthStrings.NoGrantNoRefreshToken);
@@ -108,7 +106,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements {
tokenRequest.AuthorizationDescription = verificationCode;
break;
case CodeOrTokenType.RefreshToken:
- var refreshTokenFormatter = RefreshToken.CreateFormatter(this.AuthorizationServer.Secret);
+ var refreshTokenFormatter = RefreshToken.CreateFormatter(this.AuthorizationServer.CryptoKeyStore);
var refreshToken = refreshTokenFormatter.Deserialize(message, tokenRequest.CodeOrToken);
tokenRequest.AuthorizationDescription = refreshToken;
break;
diff --git a/src/DotNetOpenAuth/OAuth2/ChannelElements/AuthorizationCode.cs b/src/DotNetOpenAuth/OAuth2/ChannelElements/AuthorizationCode.cs
index 76867a9..6067541 100644
--- a/src/DotNetOpenAuth/OAuth2/ChannelElements/AuthorizationCode.cs
+++ b/src/DotNetOpenAuth/OAuth2/ChannelElements/AuthorizationCode.cs
@@ -18,6 +18,11 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements {
/// </summary>
internal class AuthorizationCode : AuthorizationDataBag {
/// <summary>
+ /// The name of the bucket for symmetric keys used to sign authorization codes.
+ /// </summary>
+ internal const string AuthorizationCodeKeyBucket = "https://localhost/dnoa/oauth_authorization_code";
+
+ /// <summary>
/// The hash algorithm used on the callback URI.
/// </summary>
private readonly HashAlgorithm hasher = new SHA256Managed();
@@ -61,12 +66,13 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements {
Contract.Ensures(Contract.Result<IDataBagFormatter<AuthorizationCode>>() != null);
return new UriStyleMessageFormatter<AuthorizationCode>(
- authorizationServer.Secret,
- true,
- true,
- false,
- AuthorizationCodeBindingElement.MaximumMessageAge,
- authorizationServer.VerificationCodeNonceStore);
+ authorizationServer.CryptoKeyStore,
+ AuthorizationCodeKeyBucket,
+ signed: true,
+ encrypted: true,
+ compressed: false,
+ maximumAge: AuthorizationCodeBindingElement.MaximumMessageAge,
+ decodeOnceOnly: authorizationServer.VerificationCodeNonceStore);
}
/// <summary>
diff --git a/src/DotNetOpenAuth/OAuth2/ChannelElements/RefreshToken.cs b/src/DotNetOpenAuth/OAuth2/ChannelElements/RefreshToken.cs
index 8feb3fb..4662719 100644
--- a/src/DotNetOpenAuth/OAuth2/ChannelElements/RefreshToken.cs
+++ b/src/DotNetOpenAuth/OAuth2/ChannelElements/RefreshToken.cs
@@ -8,6 +8,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements {
using System;
using System.Diagnostics.Contracts;
using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
/// <summary>
/// The refresh token issued to a client by an authorization server that allows the client
@@ -15,6 +16,11 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements {
/// </summary>
internal class RefreshToken : AuthorizationDataBag {
/// <summary>
+ /// The name of the bucket for symmetric keys used to sign refresh tokens.
+ /// </summary>
+ internal const string RefreshTokenKeyBucket = "https://localhost/dnoa/oauth_refresh_token";
+
+ /// <summary>
/// Initializes a new instance of the <see cref="RefreshToken"/> class.
/// </summary>
public RefreshToken() {
@@ -36,14 +42,15 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements {
/// <summary>
/// Creates a formatter capable of serializing/deserializing a refresh token.
/// </summary>
- /// <param name="symmetricSecret">The symmetric secret used by the authorization server to sign/encrypt refresh tokens. Must not be null.</param>
- /// <returns>A DataBag formatter. Never null.</returns>
- internal static IDataBagFormatter<RefreshToken> CreateFormatter(byte[] symmetricSecret)
- {
- Contract.Requires<ArgumentNullException>(symmetricSecret != null, "symmetricSecret");
+ /// <param name="cryptoKeyStore">The crypto key store.</param>
+ /// <returns>
+ /// A DataBag formatter. Never null.
+ /// </returns>
+ internal static IDataBagFormatter<RefreshToken> CreateFormatter(ICryptoKeyStore cryptoKeyStore) {
+ Contract.Requires<ArgumentNullException>(cryptoKeyStore != null, "cryptoKeyStore");
Contract.Ensures(Contract.Result<IDataBagFormatter<RefreshToken>>() != null);
- return new UriStyleMessageFormatter<RefreshToken>(symmetricSecret, true, true);
+ return new UriStyleMessageFormatter<RefreshToken>(cryptoKeyStore, RefreshTokenKeyBucket, signed: true, encrypted: true);
}
}
}
diff --git a/src/DotNetOpenAuth/OAuth2/IAuthorizationServer.cs b/src/DotNetOpenAuth/OAuth2/IAuthorizationServer.cs
index 9a62277..d35373b 100644
--- a/src/DotNetOpenAuth/OAuth2/IAuthorizationServer.cs
+++ b/src/DotNetOpenAuth/OAuth2/IAuthorizationServer.cs
@@ -20,14 +20,14 @@ namespace DotNetOpenAuth.OAuth2 {
[ContractClass(typeof(IAuthorizationServerContract))]
public interface IAuthorizationServer {
/// <summary>
- /// Gets the secret used to symmetrically encrypt and sign authorization codes and refresh tokens.
+ /// Gets the store for storeing crypto keys used to symmetrically encrypt and sign authorization codes and refresh tokens.
/// </summary>
/// <remarks>
- /// This secret should be kept strictly confidential in the authorization server(s)
- /// and NOT shared with the resource server. Anyone with this secret can mint
+ /// This store should be kept strictly confidential in the authorization server(s)
+ /// and NOT shared with the resource server. Anyone with these secrets can mint
/// tokens to essentially grant themselves access to anything they want.
/// </remarks>
- byte[] Secret { get; }
+ ICryptoKeyStore CryptoKeyStore { get; }
/// <summary>
/// Gets the authorization code nonce store to use to ensure that authorization codes can only be used once.
@@ -92,17 +92,11 @@ namespace DotNetOpenAuth.OAuth2 {
}
/// <summary>
- /// Gets the secret used to symmetrically encrypt and sign authorization codes and refresh tokens.
+ /// Gets the store for storeing crypto keys used to symmetrically encrypt and sign authorization codes and refresh tokens.
/// </summary>
- /// <value></value>
- /// <remarks>
- /// This secret should be kept strictly confidential in the authorization server(s)
- /// and NOT shared with the resource server. Anyone with this secret can mint
- /// tokens to essentially grant themselves access to anything they want.
- /// </remarks>
- byte[] IAuthorizationServer.Secret {
+ ICryptoKeyStore IAuthorizationServer.CryptoKeyStore {
get {
- Contract.Ensures(Contract.Result<byte[]>() != null);
+ Contract.Ensures(Contract.Result<ICryptoKeyStore>() != null);
throw new NotImplementedException();
}
}
diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs
index 702e947..2e95436 100644
--- a/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs
+++ b/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs
@@ -164,7 +164,10 @@ namespace DotNetOpenAuth.OpenId.ChannelElements {
/// Gets the return to signature.
/// </summary>
/// <param name="returnTo">The return to.</param>
- /// <returns>The generated signature.</returns>
+ /// <param name="cryptoKey">The crypto key.</param>
+ /// <returns>
+ /// The generated signature.
+ /// </returns>
/// <remarks>
/// Only the parameters in the return_to URI are signed, rather than the base URI
/// itself, in order that OPs that might change the return_to's implicit port :80 part
diff --git a/src/DotNetOpenAuth/OpenId/Provider/AssociationDataBag.cs b/src/DotNetOpenAuth/OpenId/Provider/AssociationDataBag.cs
index 3caf05e..72949d9 100644
--- a/src/DotNetOpenAuth/OpenId/Provider/AssociationDataBag.cs
+++ b/src/DotNetOpenAuth/OpenId/Provider/AssociationDataBag.cs
@@ -7,10 +7,12 @@
namespace DotNetOpenAuth.OpenId.Provider {
using System;
using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Text;
using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
/// <summary>
/// A signed and encrypted serialization of an association.
@@ -77,10 +79,17 @@ namespace DotNetOpenAuth.OpenId.Provider {
/// <summary>
/// Creates the formatter used for serialization of this type.
/// </summary>
- /// <param name="symmetricSecret">The OpenID Provider's private symmetric secret to use to encrypt and sign the association data.</param>
- /// <returns>A formatter for serialization.</returns>
- internal static IDataBagFormatter<AssociationDataBag> CreateFormatter(byte[] symmetricSecret) {
- return new BinaryDataBagFormatter<AssociationDataBag>(symmetricSecret, signed: true, encrypted: true);
+ /// <param name="cryptoKeyStore">The crypto key store used when signing or encrypting.</param>
+ /// <param name="bucket">The bucket in which symmetric keys are stored for signing/encrypting data.</param>
+ /// <param name="minimumAge">The minimum age.</param>
+ /// <returns>
+ /// A formatter for serialization.
+ /// </returns>
+ internal static IDataBagFormatter<AssociationDataBag> CreateFormatter(ICryptoKeyStore cryptoKeyStore, string bucket, TimeSpan? minimumAge = null) {
+ Contract.Requires<ArgumentNullException>(cryptoKeyStore != null, "cryptoKeyStore");
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(bucket));
+ Contract.Ensures(Contract.Result<IDataBagFormatter<AssociationDataBag>>() != null);
+ return new BinaryDataBagFormatter<AssociationDataBag>(cryptoKeyStore, bucket, signed: true, encrypted: true, minimumAge: minimumAge);
}
}
}
diff --git a/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs b/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs
index e8c8881..4a52728 100644
--- a/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs
+++ b/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs
@@ -74,6 +74,7 @@ namespace DotNetOpenAuth.OpenId.Provider {
/// Initializes a new instance of the <see cref="OpenIdProvider"/> class.
/// </summary>
/// <param name="nonceStore">The nonce store to use. Cannot be null.</param>
+ /// <param name="cryptoKeyStore">The crypto key store. Cannot be null.</param>
private OpenIdProvider(INonceStore nonceStore, ICryptoKeyStore cryptoKeyStore) {
Contract.Requires<ArgumentNullException>(nonceStore != null, "nonceStore");
Contract.Requires<ArgumentNullException>(cryptoKeyStore != null, "associationStore");
diff --git a/src/DotNetOpenAuth/OpenId/Provider/ProviderAssociationHandleEncoder.cs b/src/DotNetOpenAuth/OpenId/Provider/ProviderAssociationHandleEncoder.cs
index 358daf4..73d8b56 100644
--- a/src/DotNetOpenAuth/OpenId/Provider/ProviderAssociationHandleEncoder.cs
+++ b/src/DotNetOpenAuth/OpenId/Provider/ProviderAssociationHandleEncoder.cs
@@ -17,13 +17,20 @@ namespace DotNetOpenAuth.OpenId.Provider {
/// details in the handle.
/// </summary>
public class ProviderAssociationHandleEncoder : IProviderAssociationStore {
+ /// <summary>
+ /// The name of the bucket in which to store keys that encrypt association data into association handles.
+ /// </summary>
internal const string AssociationHandleEncodingSecretBucket = "https://localhost/dnoa/association_handles";
+ /// <summary>
+ /// The crypto key store used to persist encryption keys.
+ /// </summary>
private readonly ICryptoKeyStore cryptoKeyStore;
/// <summary>
/// Initializes a new instance of the <see cref="ProviderAssociationHandleEncoder"/> class.
/// </summary>
+ /// <param name="cryptoKeyStore">The crypto key store.</param>
public ProviderAssociationHandleEncoder(ICryptoKeyStore cryptoKeyStore) {
Contract.Requires<ArgumentNullException>(cryptoKeyStore != null, "cryptoKeyStore");
this.cryptoKeyStore = cryptoKeyStore;
@@ -45,9 +52,8 @@ namespace DotNetOpenAuth.OpenId.Provider {
ExpiresUtc = expiresUtc,
};
- var encodingSecret = this.cryptoKeyStore.GetCurrentKey(AssociationHandleEncodingSecretBucket, expiresUtc - DateTime.UtcNow);
- var formatter = AssociationDataBag.CreateFormatter(encodingSecret.Value.Key);
- return encodingSecret.Key + "!" + formatter.Serialize(associationDataBag);
+ var formatter = AssociationDataBag.CreateFormatter(this.cryptoKeyStore, AssociationHandleEncodingSecretBucket, expiresUtc - DateTime.UtcNow);
+ return formatter.Serialize(associationDataBag);
}
/// <summary>
@@ -61,21 +67,12 @@ namespace DotNetOpenAuth.OpenId.Provider {
/// </returns>
/// <exception cref="ProtocolException">Thrown if the association is not of the expected type.</exception>
public Association Deserialize(IProtocolMessage containingMessage, bool isPrivateAssociation, string handle) {
- int privateHandleIndex = handle.IndexOf('!');
- ErrorUtilities.VerifyProtocol(privateHandleIndex > 0, MessagingStrings.UnexpectedMessagePartValue, containingMessage.GetProtocol().openid.assoc_handle, handle);
- string privateHandle = handle.Substring(0, privateHandleIndex);
- string encodedHandle = handle.Substring(privateHandleIndex + 1);
- var encodingSecret = this.cryptoKeyStore.GetKey(AssociationHandleEncodingSecretBucket, privateHandle);
- if (encodingSecret == null) {
- Logger.OpenId.Error("Rejecting an association because the symmetric secret it was encoded with is missing or has expired.");
- return null;
- }
-
- var formatter = AssociationDataBag.CreateFormatter(encodingSecret.Key);
+ var formatter = AssociationDataBag.CreateFormatter(this.cryptoKeyStore, AssociationHandleEncodingSecretBucket);
AssociationDataBag bag;
try {
- bag = formatter.Deserialize(containingMessage, encodedHandle);
- } catch (ProtocolException) {
+ bag = formatter.Deserialize(containingMessage, handle);
+ } catch (ProtocolException ex) {
+ Logger.OpenId.Error("Rejecting an association because deserialization of the encoded handle failed.", ex);
return null;
}
diff --git a/src/DotNetOpenAuth/OpenId/Provider/StandardProviderApplicationStore.cs b/src/DotNetOpenAuth/OpenId/Provider/StandardProviderApplicationStore.cs
index 265b555..c13c4bc 100644
--- a/src/DotNetOpenAuth/OpenId/Provider/StandardProviderApplicationStore.cs
+++ b/src/DotNetOpenAuth/OpenId/Provider/StandardProviderApplicationStore.cs
@@ -6,6 +6,7 @@
namespace DotNetOpenAuth.OpenId.Provider {
using System;
+ using System.Collections.Generic;
using DotNetOpenAuth.Configuration;
using DotNetOpenAuth.Messaging.Bindings;
@@ -27,6 +28,9 @@ namespace DotNetOpenAuth.OpenId.Provider {
/// </summary>
private readonly INonceStore nonceStore;
+ /// <summary>
+ /// The crypto key store where symmetric keys are persisted.
+ /// </summary>
private readonly ICryptoKeyStore cryptoKeyStore;
/// <summary>
@@ -65,18 +69,45 @@ namespace DotNetOpenAuth.OpenId.Provider {
#region ICryptoKeyStore
+ /// <summary>
+ /// Gets the key in a given bucket and handle.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <param name="handle">The key handle. Case sensitive.</param>
+ /// <returns>
+ /// The cryptographic key, or <c>null</c> if no matching key was found.
+ /// </returns>
public CryptoKey GetKey(string bucket, string handle) {
return this.cryptoKeyStore.GetKey(bucket, handle);
}
- public System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string, CryptoKey>> GetKeys(string bucket) {
+ /// <summary>
+ /// Gets a sequence of existing keys within a given bucket.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <returns>
+ /// A sequence of handles and keys, ordered by descending <see cref="CryptoKey.ExpiresUtc"/>.
+ /// </returns>
+ public IEnumerable<KeyValuePair<string, CryptoKey>> GetKeys(string bucket) {
return this.cryptoKeyStore.GetKeys(bucket);
}
+ /// <summary>
+ /// Stores a cryptographic key.
+ /// </summary>
+ /// <param name="bucket">The name of the bucket to store the key in. Case sensitive.</param>
+ /// <param name="handle">The handle to the key, unique within the bucket. Case sensitive.</param>
+ /// <param name="key">The key to store.</param>
+ /// <exception cref="CryptoKeyCollisionException">Thrown in the event of a conflict with an existing key in the same bucket and with the same handle.</exception>
public void StoreKey(string bucket, string handle, CryptoKey key) {
this.cryptoKeyStore.StoreKey(bucket, handle, key);
}
+ /// <summary>
+ /// Removes the key.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <param name="handle">The key handle. Case sensitive.</param>
public void RemoveKey(string bucket, string handle) {
this.cryptoKeyStore.RemoveKey(bucket, handle);
}