|
|
|
@ -61,6 +61,8 @@ namespace OpenIddict.Core |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Creates a new application.
|
|
|
|
/// Note: the default implementation automatically hashes the client
|
|
|
|
/// secret before storing it in the database, for security reasons.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="application">The application to create.</param>
|
|
|
|
/// <param name="secret">The client secret associated with the application, if applicable.</param>
|
|
|
|
@ -78,7 +80,7 @@ namespace OpenIddict.Core |
|
|
|
throw new ArgumentNullException(nameof(application)); |
|
|
|
} |
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(await Store.GetHashedSecretAsync(application, cancellationToken))) |
|
|
|
if (!string.IsNullOrEmpty(await Store.GetClientSecretAsync(application, cancellationToken))) |
|
|
|
{ |
|
|
|
throw new ArgumentException("The client secret hash cannot be directly set on the application entity."); |
|
|
|
} |
|
|
|
@ -101,12 +103,13 @@ namespace OpenIddict.Core |
|
|
|
throw new InvalidOperationException("A client secret must be provided when creating a confidential application."); |
|
|
|
} |
|
|
|
|
|
|
|
await Store.SetHashedSecretAsync(application, null, cancellationToken); |
|
|
|
await Store.SetClientSecretAsync(application, null, cancellationToken); |
|
|
|
} |
|
|
|
|
|
|
|
else |
|
|
|
{ |
|
|
|
await Store.SetHashedSecretAsync(application, Crypto.HashPassword(secret), cancellationToken); |
|
|
|
secret = await ObfuscateClientSecretAsync(secret, cancellationToken); |
|
|
|
await Store.SetClientSecretAsync(application, secret, cancellationToken); |
|
|
|
} |
|
|
|
|
|
|
|
await ValidateAsync(application, cancellationToken); |
|
|
|
@ -309,7 +312,7 @@ namespace OpenIddict.Core |
|
|
|
throw new ArgumentNullException(nameof(application)); |
|
|
|
} |
|
|
|
|
|
|
|
return !string.IsNullOrEmpty(await Store.GetHashedSecretAsync(application, cancellationToken)); |
|
|
|
return !string.IsNullOrEmpty(await Store.GetClientSecretAsync(application, cancellationToken)); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
@ -358,15 +361,37 @@ namespace OpenIddict.Core |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Updates the client secret associated with an application.
|
|
|
|
/// Updates an existing application.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="application">The application.</param>
|
|
|
|
/// <param name="application">The application to update.</param>
|
|
|
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
|
|
|
/// <returns>
|
|
|
|
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
|
|
|
|
/// </returns>
|
|
|
|
public virtual async Task UpdateAsync([NotNull] TApplication application, CancellationToken cancellationToken) |
|
|
|
{ |
|
|
|
if (application == null) |
|
|
|
{ |
|
|
|
throw new ArgumentNullException(nameof(application)); |
|
|
|
} |
|
|
|
|
|
|
|
await ValidateAsync(application, cancellationToken); |
|
|
|
await Store.UpdateAsync(application, cancellationToken); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Updates an existing application and replaces the existing secret.
|
|
|
|
/// Note: the default implementation automatically hashes the client
|
|
|
|
/// secret before storing it in the database, for security reasons.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="application">The application to update.</param>
|
|
|
|
/// <param name="secret">The client secret associated with the application.</param>
|
|
|
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
|
|
|
/// <returns>
|
|
|
|
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
|
|
|
|
/// </returns>
|
|
|
|
public virtual async Task SetClientSecretAsync([NotNull] TApplication application, [CanBeNull] string secret, CancellationToken cancellationToken) |
|
|
|
public virtual async Task UpdateAsync([NotNull] TApplication application, |
|
|
|
[CanBeNull] string secret, CancellationToken cancellationToken) |
|
|
|
{ |
|
|
|
if (application == null) |
|
|
|
{ |
|
|
|
@ -375,64 +400,119 @@ namespace OpenIddict.Core |
|
|
|
|
|
|
|
if (string.IsNullOrEmpty(secret)) |
|
|
|
{ |
|
|
|
await Store.SetHashedSecretAsync(application, null, cancellationToken); |
|
|
|
await Store.SetClientSecretAsync(application, null, cancellationToken); |
|
|
|
} |
|
|
|
|
|
|
|
else |
|
|
|
{ |
|
|
|
await Store.SetHashedSecretAsync(application, Crypto.HashPassword(secret), cancellationToken); |
|
|
|
secret = await ObfuscateClientSecretAsync(secret, cancellationToken); |
|
|
|
await Store.SetClientSecretAsync(application, secret, cancellationToken); |
|
|
|
} |
|
|
|
|
|
|
|
await UpdateAsync(application, cancellationToken); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Updates an existing application.
|
|
|
|
/// Validates the specified post_logout_redirect_uri.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="application">The application to update.</param>
|
|
|
|
/// <param name="address">The address that should be compared to the post_logout_redirect_uri stored in the database.</param>
|
|
|
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
|
|
|
/// <returns>
|
|
|
|
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
|
|
|
|
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
|
|
|
|
/// returns a boolean indicating whether the post_logout_redirect_uri was valid.
|
|
|
|
/// </returns>
|
|
|
|
public virtual async Task UpdateAsync([NotNull] TApplication application, CancellationToken cancellationToken) |
|
|
|
public virtual async Task<bool> ValidateLogoutRedirectUriAsync(string address, CancellationToken cancellationToken) |
|
|
|
{ |
|
|
|
// Warning: SQL engines like Microsoft SQL Server are known to use case-insensitive lookups by default.
|
|
|
|
// To ensure a case-sensitive comparison is used, string.Equals(Ordinal) is manually called here.
|
|
|
|
foreach (var application in await Store.FindByLogoutRedirectUriAsync(address, cancellationToken)) |
|
|
|
{ |
|
|
|
// Note: the post_logout_redirect_uri must be compared using case-sensitive "Simple String Comparison".
|
|
|
|
if (string.Equals(address, await Store.GetLogoutRedirectUriAsync(application, cancellationToken), StringComparison.Ordinal)) |
|
|
|
{ |
|
|
|
return true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
Logger.LogWarning("Client validation failed because '{PostLogoutRedirectUri}' " + |
|
|
|
"was not a valid post_logout_redirect_uri.", address); |
|
|
|
|
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Validates the redirect_uri associated with an application.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="application">The application.</param>
|
|
|
|
/// <param name="address">The address that should be compared to the redirect_uri stored in the database.</param>
|
|
|
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
|
|
|
/// <returns>
|
|
|
|
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
|
|
|
|
/// whose result returns a boolean indicating whether the redirect_uri was valid.
|
|
|
|
/// </returns>
|
|
|
|
public virtual async Task<bool> ValidateRedirectUriAsync([NotNull] TApplication application, string address, CancellationToken cancellationToken) |
|
|
|
{ |
|
|
|
if (application == null) |
|
|
|
{ |
|
|
|
throw new ArgumentNullException(nameof(application)); |
|
|
|
} |
|
|
|
|
|
|
|
await ValidateAsync(application, cancellationToken); |
|
|
|
await Store.UpdateAsync(application, cancellationToken); |
|
|
|
// Note: the redirect_uri must be compared using case-sensitive "Simple String Comparison".
|
|
|
|
// See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest for more information.
|
|
|
|
if (string.Equals(address, await Store.GetRedirectUriAsync(application, cancellationToken), StringComparison.Ordinal)) |
|
|
|
{ |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
Logger.LogWarning("Client validation failed because '{RedirectUri}' was not a valid redirect_uri " + |
|
|
|
"for '{Client}'.", address, await GetDisplayNameAsync(application, cancellationToken)); |
|
|
|
|
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Updates an existing application.
|
|
|
|
/// Validates the client_secret associated with an application.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="application">The application to update.</param>
|
|
|
|
/// <param name="secret">The client secret associated with the application.</param>
|
|
|
|
/// <param name="application">The application.</param>
|
|
|
|
/// <param name="secret">The secret that should be compared to the client_secret stored in the database.</param>
|
|
|
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
|
|
|
/// <returns>A <see cref="Task"/> that can be used to monitor the asynchronous operation.</returns>
|
|
|
|
/// <returns>
|
|
|
|
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
|
|
|
|
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
|
|
|
|
/// whose result returns a boolean indicating whether the client secret was valid.
|
|
|
|
/// </returns>
|
|
|
|
public virtual async Task UpdateAsync([NotNull] TApplication application, |
|
|
|
[CanBeNull] string secret, CancellationToken cancellationToken) |
|
|
|
public virtual async Task<bool> ValidateClientSecretAsync([NotNull] TApplication application, string secret, CancellationToken cancellationToken) |
|
|
|
{ |
|
|
|
if (application == null) |
|
|
|
{ |
|
|
|
throw new ArgumentNullException(nameof(application)); |
|
|
|
} |
|
|
|
|
|
|
|
if (string.IsNullOrEmpty(secret)) |
|
|
|
if (!await IsConfidentialAsync(application, cancellationToken)) |
|
|
|
{ |
|
|
|
await Store.SetHashedSecretAsync(application, null, cancellationToken); |
|
|
|
Logger.LogWarning("Client authentication cannot be enforced for non-confidential applications."); |
|
|
|
|
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
else |
|
|
|
var value = await Store.GetClientSecretAsync(application, cancellationToken); |
|
|
|
if (string.IsNullOrEmpty(value)) |
|
|
|
{ |
|
|
|
await Store.SetHashedSecretAsync(application, Crypto.HashPassword(secret), cancellationToken); |
|
|
|
Logger.LogError("Client authentication failed for {Client} because " + |
|
|
|
"no client secret was associated with the application."); |
|
|
|
|
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
await UpdateAsync(application, cancellationToken); |
|
|
|
if (!await ValidateClientSecretAsync(secret, value, cancellationToken)) |
|
|
|
{ |
|
|
|
Logger.LogWarning("Client authentication failed for {Client}.", |
|
|
|
await GetDisplayNameAsync(application, cancellationToken)); |
|
|
|
|
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
@ -469,17 +549,17 @@ namespace OpenIddict.Core |
|
|
|
"supported by the default application manager.", nameof(application)); |
|
|
|
} |
|
|
|
|
|
|
|
var hash = await Store.GetHashedSecretAsync(application, cancellationToken); |
|
|
|
var secret = await Store.GetClientSecretAsync(application, cancellationToken); |
|
|
|
|
|
|
|
// Ensure a client secret was specified if the client is a confidential application.
|
|
|
|
if (string.IsNullOrEmpty(hash) && |
|
|
|
if (string.IsNullOrEmpty(secret) && |
|
|
|
string.Equals(type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase)) |
|
|
|
{ |
|
|
|
throw new ArgumentException("The client secret cannot be null or empty for a confidential application.", nameof(application)); |
|
|
|
} |
|
|
|
|
|
|
|
// Ensure no client secret was specified if the client is a public application.
|
|
|
|
else if (!string.IsNullOrEmpty(hash) && |
|
|
|
else if (!string.IsNullOrEmpty(secret) && |
|
|
|
string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase)) |
|
|
|
{ |
|
|
|
throw new ArgumentException("A client secret cannot be associated with a public application.", nameof(application)); |
|
|
|
@ -522,106 +602,60 @@ namespace OpenIddict.Core |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Validates the specified post_logout_redirect_uri.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="address">The address that should be compared to the post_logout_redirect_uri stored in the database.</param>
|
|
|
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
|
|
|
/// <returns>
|
|
|
|
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
|
|
|
|
/// returns a boolean indicating whether the post_logout_redirect_uri was valid.
|
|
|
|
/// </returns>
|
|
|
|
public virtual async Task<bool> ValidateLogoutRedirectUriAsync(string address, CancellationToken cancellationToken) |
|
|
|
{ |
|
|
|
// Warning: SQL engines like Microsoft SQL Server are known to use case-insensitive lookups by default.
|
|
|
|
// To ensure a case-sensitive comparison is used, string.Equals(Ordinal) is manually called here.
|
|
|
|
foreach (var application in await Store.FindByLogoutRedirectUriAsync(address, cancellationToken)) |
|
|
|
{ |
|
|
|
// Note: the post_logout_redirect_uri must be compared using case-sensitive "Simple String Comparison".
|
|
|
|
if (string.Equals(address, await Store.GetLogoutRedirectUriAsync(application, cancellationToken), StringComparison.Ordinal)) |
|
|
|
{ |
|
|
|
return true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
Logger.LogWarning("Client validation failed because '{PostLogoutRedirectUri}' " + |
|
|
|
"was not a valid post_logout_redirect_uri.", address); |
|
|
|
|
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Validates the redirect_uri associated with an application.
|
|
|
|
/// Obfuscates the specified client secret so it can be safely stored in a database.
|
|
|
|
/// By default, this method returns a complex hashed representation computed using PBKDF2.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="application">The application.</param>
|
|
|
|
/// <param name="address">The address that should be compared to the redirect_uri stored in the database.</param>
|
|
|
|
/// <param name="secret">The client secret.</param>
|
|
|
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
|
|
|
/// <returns>
|
|
|
|
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
|
|
|
|
/// whose result returns a boolean indicating whether the redirect_uri was valid.
|
|
|
|
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
|
|
|
|
/// </returns>
|
|
|
|
public virtual async Task<bool> ValidateRedirectUriAsync([NotNull] TApplication application, string address, CancellationToken cancellationToken) |
|
|
|
protected virtual Task<string> ObfuscateClientSecretAsync([NotNull] string secret, CancellationToken cancellationToken) |
|
|
|
{ |
|
|
|
if (application == null) |
|
|
|
{ |
|
|
|
throw new ArgumentNullException(nameof(application)); |
|
|
|
} |
|
|
|
|
|
|
|
// Note: the redirect_uri must be compared using case-sensitive "Simple String Comparison".
|
|
|
|
// See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest for more information.
|
|
|
|
if (string.Equals(address, await Store.GetRedirectUriAsync(application, cancellationToken), StringComparison.Ordinal)) |
|
|
|
if (string.IsNullOrEmpty(secret)) |
|
|
|
{ |
|
|
|
return true; |
|
|
|
throw new ArgumentException("The secret cannot be null or empty.", nameof(secret)); |
|
|
|
} |
|
|
|
|
|
|
|
Logger.LogWarning("Client validation failed because '{RedirectUri}' was not a valid redirect_uri " + |
|
|
|
"for '{Client}'.", address, await GetDisplayNameAsync(application, cancellationToken)); |
|
|
|
|
|
|
|
return false; |
|
|
|
return Task.FromResult(Crypto.HashPassword(secret)); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Validates the client_secret associated with an application.
|
|
|
|
/// Validates the specified value to ensure it corresponds to the client secret.
|
|
|
|
/// Note: when overriding this method, using a time-constant comparer is strongly recommended.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="application">The application.</param>
|
|
|
|
/// <param name="secret">The secret that should be compared to the client_secret stored in the database.</param>
|
|
|
|
/// <param name="secret">The client secret to compare to the value stored in the database.</param>
|
|
|
|
/// <param name="comparand">The value stored in the database, which is usually a hashed representation of the secret.</param>
|
|
|
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
|
|
|
/// <returns>A <see cref="Task"/> that can be used to monitor the asynchronous operation.</returns>
|
|
|
|
/// <returns>
|
|
|
|
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
|
|
|
|
/// whose result returns a boolean indicating whether the client secret was valid.
|
|
|
|
/// whose result returns a boolean indicating whether the specified value was valid.
|
|
|
|
/// </returns>
|
|
|
|
public virtual async Task<bool> ValidateClientSecretAsync([NotNull] TApplication application, string secret, CancellationToken cancellationToken) |
|
|
|
protected virtual Task<bool> ValidateClientSecretAsync( |
|
|
|
[NotNull] string secret, [NotNull] string comparand, CancellationToken cancellationToken) |
|
|
|
{ |
|
|
|
if (application == null) |
|
|
|
if (string.IsNullOrEmpty(secret)) |
|
|
|
{ |
|
|
|
throw new ArgumentNullException(nameof(application)); |
|
|
|
throw new ArgumentException("The secret cannot be null or empty.", nameof(secret)); |
|
|
|
} |
|
|
|
|
|
|
|
if (!await IsConfidentialAsync(application, cancellationToken)) |
|
|
|
if (string.IsNullOrEmpty(comparand)) |
|
|
|
{ |
|
|
|
Logger.LogWarning("Client authentication cannot be enforced for non-confidential applications."); |
|
|
|
|
|
|
|
return false; |
|
|
|
throw new ArgumentException("The comparand cannot be null or empty.", nameof(comparand)); |
|
|
|
} |
|
|
|
|
|
|
|
var hash = await Store.GetHashedSecretAsync(application, cancellationToken); |
|
|
|
if (string.IsNullOrEmpty(hash)) |
|
|
|
try |
|
|
|
{ |
|
|
|
Logger.LogError("Client authentication failed for {Client} because " + |
|
|
|
"no client secret was associated with the application."); |
|
|
|
|
|
|
|
return false; |
|
|
|
return Task.FromResult(Crypto.VerifyHashedPassword(comparand, secret)); |
|
|
|
} |
|
|
|
|
|
|
|
if (!Crypto.VerifyHashedPassword(hash, secret)) |
|
|
|
catch (Exception exception) |
|
|
|
{ |
|
|
|
Logger.LogWarning("Client authentication failed for {Client}.", |
|
|
|
await GetDisplayNameAsync(application, cancellationToken)); |
|
|
|
Logger.LogWarning(0, exception, "An error occurred while trying to verify a client secret. " + |
|
|
|
"This may indicate that the hashed entry is corrupted or malformed."); |
|
|
|
|
|
|
|
return false; |
|
|
|
return Task.FromResult(false); |
|
|
|
} |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
} |
|
|
|
} |