Browse Source

Add extra checks in the managers to ensure case-sensitive comparisons are enforced independently of the database/table/query collation

pull/583/head
Kévin Chalet 8 years ago
parent
commit
950bbc5ed0
  1. 132
      src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
  2. 138
      src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
  3. 59
      src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs
  4. 60
      src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
  5. 8
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs
  6. 8
      src/OpenIddict.Stores/Stores/OpenIddictApplicationStore.cs

132
src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs

@ -230,14 +230,25 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
public virtual Task<TApplication> FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default)
public virtual async Task<TApplication> FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
return Store.FindByClientIdAsync(identifier, cancellationToken);
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
var application = await Store.FindByClientIdAsync(identifier, cancellationToken);
if (application == null ||
!string.Equals(await Store.GetClientIdAsync(application, cancellationToken), identifier, StringComparison.Ordinal))
{
return null;
}
return application;
}
/// <summary>
@ -249,7 +260,7 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified post_logout_redirect_uri.
/// </returns>
public virtual Task<ImmutableArray<TApplication>> FindByPostLogoutRedirectUriAsync(
public virtual async Task<ImmutableArray<TApplication>> FindByPostLogoutRedirectUriAsync(
[NotNull] string address, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(address))
@ -257,7 +268,33 @@ namespace OpenIddict.Core
throw new ArgumentException("The address cannot be null or empty.", nameof(address));
}
return Store.FindByPostLogoutRedirectUriAsync(address, cancellationToken);
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
var applications = await Store.FindByPostLogoutRedirectUriAsync(address, cancellationToken);
if (applications.IsEmpty)
{
return ImmutableArray.Create<TApplication>();
}
var builder = ImmutableArray.CreateBuilder<TApplication>(applications.Length);
foreach (var application in applications)
{
foreach (var uri in await Store.GetPostLogoutRedirectUrisAsync(application, cancellationToken))
{
// Note: the post_logout_redirect_uri must be compared using case-sensitive "Simple String Comparison".
if (string.Equals(uri, address, StringComparison.Ordinal))
{
builder.Add(application);
}
}
}
return builder.Count == builder.Capacity ?
builder.MoveToImmutable() :
builder.ToImmutable();
}
/// <summary>
@ -269,7 +306,7 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified redirect_uri.
/// </returns>
public virtual Task<ImmutableArray<TApplication>> FindByRedirectUriAsync(
public virtual async Task<ImmutableArray<TApplication>> FindByRedirectUriAsync(
[NotNull] string address, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(address))
@ -277,7 +314,33 @@ namespace OpenIddict.Core
throw new ArgumentException("The address cannot be null or empty.", nameof(address));
}
return Store.FindByRedirectUriAsync(address, cancellationToken);
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
var applications = await Store.FindByRedirectUriAsync(address, cancellationToken);
if (applications.IsEmpty)
{
return ImmutableArray.Create<TApplication>();
}
var builder = ImmutableArray.CreateBuilder<TApplication>(applications.Length);
foreach (var application in applications)
{
foreach (var uri in await Store.GetRedirectUrisAsync(application, cancellationToken))
{
// Note: the post_logout_redirect_uri must be compared using case-sensitive "Simple String Comparison".
if (string.Equals(uri, address, StringComparison.Ordinal))
{
builder.Add(application);
}
}
}
return builder.Count == builder.Capacity ?
builder.MoveToImmutable() :
builder.ToImmutable();
}
/// <summary>
@ -513,6 +576,8 @@ namespace OpenIddict.Core
throw new ArgumentException("The permission name cannot be null or empty.", nameof(permission));
}
// Note: all the string-based comparisons used by this method are ordinal (and thus case-sensitive).
var permissions = await Store.GetPermissionsAsync(application, cancellationToken);
bool HasPermission(string name)
@ -897,30 +962,34 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(application));
}
var results = ImmutableArray.CreateBuilder<ValidationResult>();
var builder = ImmutableArray.CreateBuilder<ValidationResult>();
// Ensure the client_id is not null or empty and is not already used for a different application.
var identifier = await Store.GetClientIdAsync(application, cancellationToken);
if (string.IsNullOrEmpty(identifier))
{
results.Add(new ValidationResult("The client identifier cannot be null or empty."));
builder.Add(new ValidationResult("The client identifier cannot be null or empty."));
}
else
{
// Ensure the client_id is not already used for a different application.
// Note: depending on the database/table/query collation used by the store, an application
// whose client_id doesn't exactly match the specified value may be returned (e.g because
// the casing is different). To avoid issues when the client identifier is part of an index
// using the same collation, an error is added even if the two identifiers don't exactly match.
var other = await Store.FindByClientIdAsync(identifier, cancellationToken);
if (other != null && !string.Equals(
await Store.GetIdAsync(other, cancellationToken),
await Store.GetIdAsync(application, cancellationToken), StringComparison.Ordinal))
{
results.Add(new ValidationResult("An application with the same client identifier already exists."));
builder.Add(new ValidationResult("An application with the same client identifier already exists."));
}
}
var type = await Store.GetClientTypeAsync(application, cancellationToken);
if (string.IsNullOrEmpty(type))
{
results.Add(new ValidationResult("The client type cannot be null or empty."));
builder.Add(new ValidationResult("The client type cannot be null or empty."));
}
else
@ -930,7 +999,7 @@ namespace OpenIddict.Core
!string.Equals(type, OpenIddictConstants.ClientTypes.Hybrid, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase))
{
results.Add(new ValidationResult("Only 'confidential', 'hybrid' or 'public' applications are " +
builder.Add(new ValidationResult("Only 'confidential', 'hybrid' or 'public' applications are " +
"supported by the default application manager."));
}
@ -939,14 +1008,14 @@ namespace OpenIddict.Core
if (string.IsNullOrEmpty(secret) &&
string.Equals(type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase))
{
results.Add(new ValidationResult("The client secret cannot be null or empty for a confidential application."));
builder.Add(new ValidationResult("The client secret cannot be null or empty for a confidential application."));
}
// Ensure no client secret was specified if the client is a public application.
else if (!string.IsNullOrEmpty(secret) &&
string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase))
{
results.Add(new ValidationResult("A client secret cannot be associated with a public application."));
builder.Add(new ValidationResult("A client secret cannot be associated with a public application."));
}
}
@ -959,7 +1028,7 @@ namespace OpenIddict.Core
// Ensure the address is not null or empty.
if (string.IsNullOrEmpty(address))
{
results.Add(new ValidationResult("Callback URLs cannot be null or empty."));
builder.Add(new ValidationResult("Callback URLs cannot be null or empty."));
break;
}
@ -967,7 +1036,7 @@ namespace OpenIddict.Core
// Ensure the address is a valid absolute URL.
if (!Uri.TryCreate(address, UriKind.Absolute, out Uri uri) || !uri.IsWellFormedOriginalString())
{
results.Add(new ValidationResult("Callback URLs must be valid absolute URLs."));
builder.Add(new ValidationResult("Callback URLs must be valid absolute URLs."));
break;
}
@ -975,7 +1044,7 @@ namespace OpenIddict.Core
// Ensure the address doesn't contain a fragment.
if (!string.IsNullOrEmpty(uri.Fragment))
{
results.Add(new ValidationResult("Callback URLs cannot contain a fragment."));
builder.Add(new ValidationResult("Callback URLs cannot contain a fragment."));
break;
}
@ -987,14 +1056,14 @@ namespace OpenIddict.Core
if (!permissions.Contains(OpenIddictConstants.Permissions.Endpoints.Authorization) &&
permissions.Any(permission => permission.StartsWith(OpenIddictConstants.Permissions.Prefixes.Endpoint)))
{
results.Add(new ValidationResult(
builder.Add(new ValidationResult(
"The authorization code flow permission requires adding the authorization endpoint permission."));
}
if (!permissions.Contains(OpenIddictConstants.Permissions.Endpoints.Token) &&
permissions.Any(permission => permission.StartsWith(OpenIddictConstants.Permissions.Prefixes.Endpoint)))
{
results.Add(new ValidationResult(
builder.Add(new ValidationResult(
"The authorization code flow permission requires adding the token endpoint permission."));
}
}
@ -1003,7 +1072,7 @@ namespace OpenIddict.Core
!permissions.Contains(OpenIddictConstants.Permissions.Endpoints.Token) &&
permissions.Any(permission => permission.StartsWith(OpenIddictConstants.Permissions.Prefixes.Endpoint)))
{
results.Add(new ValidationResult(
builder.Add(new ValidationResult(
"The client credentials flow permission requires adding the token endpoint permission."));
}
@ -1011,7 +1080,7 @@ namespace OpenIddict.Core
!permissions.Contains(OpenIddictConstants.Permissions.Endpoints.Authorization) &&
permissions.Any(permission => permission.StartsWith(OpenIddictConstants.Permissions.Prefixes.Endpoint)))
{
results.Add(new ValidationResult(
builder.Add(new ValidationResult(
"The implicit flow permission requires adding the authorization endpoint permission."));
}
@ -1019,7 +1088,7 @@ namespace OpenIddict.Core
!permissions.Contains(OpenIddictConstants.Permissions.Endpoints.Token) &&
permissions.Any(permission => permission.StartsWith(OpenIddictConstants.Permissions.Prefixes.Endpoint)))
{
results.Add(new ValidationResult(
builder.Add(new ValidationResult(
"The password flow permission requires adding the token endpoint permission."));
}
@ -1027,11 +1096,13 @@ namespace OpenIddict.Core
!permissions.Contains(OpenIddictConstants.Permissions.Endpoints.Token) &&
permissions.Any(permission => permission.StartsWith(OpenIddictConstants.Permissions.Prefixes.Endpoint)))
{
results.Add(new ValidationResult(
builder.Add(new ValidationResult(
"The refresh token flow permission requires adding the token endpoint permission."));
}
return results.ToImmutable();
return builder.Count == builder.Capacity ?
builder.MoveToImmutable() :
builder.ToImmutable();
}
/// <summary>
@ -1097,9 +1168,7 @@ namespace OpenIddict.Core
throw new ArgumentException("The address cannot be null or empty.", nameof(address));
}
// 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.FindByPostLogoutRedirectUriAsync(address, cancellationToken))
foreach (var application in await FindByPostLogoutRedirectUriAsync(address, cancellationToken))
{
// If the application is not allowed to use the logout endpoint, ignore it and keep iterating.
if (!await HasPermissionAsync(application, OpenIddictConstants.Permissions.Endpoints.Logout, cancellationToken))
@ -1107,14 +1176,7 @@ namespace OpenIddict.Core
continue;
}
foreach (var uri in await Store.GetPostLogoutRedirectUrisAsync(application, cancellationToken))
{
// Note: the post_logout_redirect_uri must be compared using case-sensitive "Simple String Comparison".
if (string.Equals(uri, address, StringComparison.Ordinal))
{
return true;
}
}
return true;
}
Logger.LogWarning("Client validation failed because '{PostLogoutRedirectUri}' " +

138
src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs

@ -222,7 +222,7 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the subject/client.
/// </returns>
public virtual Task<ImmutableArray<TAuthorization>> FindAsync(
public virtual async Task<ImmutableArray<TAuthorization>> FindAsync(
[NotNull] string subject, [NotNull] string client, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(subject))
@ -235,7 +235,29 @@ namespace OpenIddict.Core
throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client));
}
return Store.FindAsync(subject, client, cancellationToken);
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
var authorizations = await Store.FindAsync(subject, client, cancellationToken);
if (authorizations.IsEmpty)
{
return ImmutableArray.Create<TAuthorization>();
}
var builder = ImmutableArray.CreateBuilder<TAuthorization>(authorizations.Length);
foreach (var authorization in authorizations)
{
if (string.Equals(await Store.GetSubjectAsync(authorization, cancellationToken), subject, StringComparison.Ordinal))
{
builder.Add(authorization);
}
}
return builder.Count == builder.Capacity ?
builder.MoveToImmutable() :
builder.ToImmutable();
}
/// <summary>
@ -249,7 +271,7 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the criteria.
/// </returns>
public virtual Task<ImmutableArray<TAuthorization>> FindAsync(
public virtual async Task<ImmutableArray<TAuthorization>> FindAsync(
[NotNull] string subject, [NotNull] string client,
[NotNull] string status, CancellationToken cancellationToken)
{
@ -268,7 +290,29 @@ namespace OpenIddict.Core
throw new ArgumentException("The status cannot be null or empty.", nameof(status));
}
return Store.FindAsync(subject, client, status, cancellationToken);
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
var authorizations = await Store.FindAsync(subject, client, status, cancellationToken);
if (authorizations.IsEmpty)
{
return ImmutableArray.Create<TAuthorization>();
}
var builder = ImmutableArray.CreateBuilder<TAuthorization>(authorizations.Length);
foreach (var authorization in authorizations)
{
if (string.Equals(await Store.GetSubjectAsync(authorization, cancellationToken), subject, StringComparison.Ordinal))
{
builder.Add(authorization);
}
}
return builder.Count == builder.Capacity ?
builder.MoveToImmutable() :
builder.ToImmutable();
}
/// <summary>
@ -283,7 +327,7 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the criteria.
/// </returns>
public virtual Task<ImmutableArray<TAuthorization>> FindAsync(
public virtual async Task<ImmutableArray<TAuthorization>> FindAsync(
[NotNull] string subject, [NotNull] string client,
[NotNull] string status, [NotNull] string type, CancellationToken cancellationToken = default)
{
@ -307,7 +351,29 @@ namespace OpenIddict.Core
throw new ArgumentException("The type cannot be null or empty.", nameof(type));
}
return Store.FindAsync(subject, client, status, type, cancellationToken);
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
var authorizations = await Store.FindAsync(subject, client, status, type, cancellationToken);
if (authorizations.IsEmpty)
{
return ImmutableArray.Create<TAuthorization>();
}
var builder = ImmutableArray.CreateBuilder<TAuthorization>(authorizations.Length);
foreach (var authorization in authorizations)
{
if (string.Equals(await Store.GetSubjectAsync(authorization, cancellationToken), subject, StringComparison.Ordinal))
{
builder.Add(authorization);
}
}
return builder.Count == builder.Capacity ?
builder.MoveToImmutable() :
builder.ToImmutable();
}
/// <summary>
@ -328,17 +394,25 @@ namespace OpenIddict.Core
[NotNull] string status, [NotNull] string type,
ImmutableArray<string> scopes, CancellationToken cancellationToken = default)
{
var authorizations = ImmutableArray.CreateBuilder<TAuthorization>();
var authorizations = await FindAsync(subject, client, status, type, cancellationToken);
if (authorizations.IsEmpty)
{
return ImmutableArray.Create<TAuthorization>();
}
var builder = ImmutableArray.CreateBuilder<TAuthorization>(authorizations.Length);
foreach (var authorization in await FindAsync(subject, client, status, type, cancellationToken))
foreach (var authorization in authorizations)
{
if (await HasScopesAsync(authorization, scopes, cancellationToken))
{
authorizations.Add(authorization);
builder.Add(authorization);
}
}
return authorizations.ToImmutable();
return builder.Count == builder.Capacity ?
builder.MoveToImmutable() :
builder.ToImmutable();
}
/// <summary>
@ -369,7 +443,7 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the specified subject.
/// </returns>
public virtual Task<ImmutableArray<TAuthorization>> FindBySubjectAsync(
public virtual async Task<ImmutableArray<TAuthorization>> FindBySubjectAsync(
[NotNull] string subject, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(subject))
@ -377,7 +451,29 @@ namespace OpenIddict.Core
throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
}
return Store.FindBySubjectAsync(subject, cancellationToken);
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
var authorizations = await Store.FindBySubjectAsync(subject, cancellationToken);
if (authorizations.IsEmpty)
{
return ImmutableArray.Create<TAuthorization>();
}
var builder = ImmutableArray.CreateBuilder<TAuthorization>(authorizations.Length);
foreach (var authorization in authorizations)
{
if (string.Equals(await Store.GetSubjectAsync(authorization, cancellationToken), subject, StringComparison.Ordinal))
{
builder.Add(authorization);
}
}
return builder.Count == builder.Capacity ?
builder.MoveToImmutable() :
builder.ToImmutable();
}
/// <summary>
@ -834,28 +930,28 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(authorization));
}
var results = ImmutableArray.CreateBuilder<ValidationResult>();
var builder = ImmutableArray.CreateBuilder<ValidationResult>();
var type = await Store.GetTypeAsync(authorization, cancellationToken);
if (string.IsNullOrEmpty(type))
{
results.Add(new ValidationResult("The authorization type cannot be null or empty."));
builder.Add(new ValidationResult("The authorization type cannot be null or empty."));
}
else if (!string.Equals(type, OpenIddictConstants.AuthorizationTypes.AdHoc, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(type, OpenIddictConstants.AuthorizationTypes.Permanent, StringComparison.OrdinalIgnoreCase))
{
results.Add(new ValidationResult("The specified authorization type is not supported by the default token manager."));
builder.Add(new ValidationResult("The specified authorization type is not supported by the default token manager."));
}
if (string.IsNullOrEmpty(await Store.GetStatusAsync(authorization, cancellationToken)))
{
results.Add(new ValidationResult("The status cannot be null or empty."));
builder.Add(new ValidationResult("The status cannot be null or empty."));
}
if (string.IsNullOrEmpty(await Store.GetSubjectAsync(authorization, cancellationToken)))
{
results.Add(new ValidationResult("The subject cannot be null or empty."));
builder.Add(new ValidationResult("The subject cannot be null or empty."));
}
// Ensure that the scopes are not null or empty and do not contain spaces.
@ -863,20 +959,22 @@ namespace OpenIddict.Core
{
if (string.IsNullOrEmpty(scope))
{
results.Add(new ValidationResult("Scopes cannot be null or empty."));
builder.Add(new ValidationResult("Scopes cannot be null or empty."));
break;
}
if (scope.Contains(OpenIddictConstants.Separators.Space))
{
results.Add(new ValidationResult("Scopes cannot contain spaces."));
builder.Add(new ValidationResult("Scopes cannot contain spaces."));
break;
}
}
return results.ToImmutable();
return builder.Count == builder.Capacity ?
builder.MoveToImmutable() :
builder.ToImmutable();
}
/// <summary>

59
src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs

@ -172,14 +172,24 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the specified name.
/// </returns>
public virtual Task<TScope> FindByNameAsync([NotNull] string name, CancellationToken cancellationToken = default)
public virtual async Task<TScope> FindByNameAsync([NotNull] string name, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("The scope name cannot be null or empty.", nameof(name));
}
return Store.FindByNameAsync(name, cancellationToken);
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
var scope = await Store.FindByNameAsync(name, cancellationToken);
if (scope == null || !string.Equals(await Store.GetNameAsync(scope, cancellationToken), name, StringComparison.Ordinal))
{
return null;
}
return scope;
}
/// <summary>
@ -191,7 +201,7 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes corresponding to the specified names.
/// </returns>
public virtual Task<ImmutableArray<TScope>> FindByNamesAsync(
public virtual async Task<ImmutableArray<TScope>> FindByNamesAsync(
ImmutableArray<string> names, CancellationToken cancellationToken = default)
{
if (names.Any(name => string.IsNullOrEmpty(name)))
@ -199,7 +209,29 @@ namespace OpenIddict.Core
throw new ArgumentException("Scope names cannot be null or empty.", nameof(names));
}
return Store.FindByNamesAsync(names, cancellationToken);
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
var scopes = await Store.FindByNamesAsync(names, cancellationToken);
if (scopes.IsEmpty)
{
return ImmutableArray.Create<TScope>();
}
var builder = ImmutableArray.CreateBuilder<TScope>(scopes.Length);
foreach (var scope in scopes)
{
if (names.Contains(await Store.GetNameAsync(scope, cancellationToken)))
{
builder.Add(scope);
}
}
return builder.Count == builder.Capacity ?
builder.MoveToImmutable() :
builder.ToImmutable();
}
/// <summary>
@ -499,32 +531,39 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(scope));
}
var results = ImmutableArray.CreateBuilder<ValidationResult>();
var builder = ImmutableArray.CreateBuilder<ValidationResult>();
// Ensure the name is not null or empty, does not contain a
// space and is not already used for a different scope entity.
var name = await Store.GetNameAsync(scope, cancellationToken);
if (string.IsNullOrEmpty(name))
{
results.Add(new ValidationResult("The scope name cannot be null or empty."));
builder.Add(new ValidationResult("The scope name cannot be null or empty."));
}
else if (name.Contains(OpenIddictConstants.Separators.Space))
{
results.Add(new ValidationResult("The scope name cannot contain spaces."));
builder.Add(new ValidationResult("The scope name cannot contain spaces."));
}
else
{
// Ensure the name is not already used for a different name.
// Note: depending on the database/table/query collation used by the store, a scope
// whose name doesn't exactly match the specified value may be returned (e.g because
// the casing is different). To avoid issues when the scope name is part of an index
// using the same collation, an error is added even if the two names don't exactly match.
var other = await Store.FindByNameAsync(name, cancellationToken);
if (other != null && !string.Equals(
await Store.GetIdAsync(other, cancellationToken),
await Store.GetIdAsync(scope, cancellationToken), StringComparison.Ordinal))
{
results.Add(new ValidationResult("A scope with the same name already exists."));
builder.Add(new ValidationResult("A scope with the same name already exists."));
}
}
return results.ToImmutable();
return builder.Count == builder.Capacity ?
builder.MoveToImmutable() :
builder.ToImmutable();
}
/// <summary>

60
src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs

@ -230,8 +230,20 @@ namespace OpenIddict.Core
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
identifier = await ObfuscateReferenceIdAsync(identifier, cancellationToken);
return await Store.FindByReferenceIdAsync(identifier, cancellationToken);
var token = await Store.FindByReferenceIdAsync(identifier, cancellationToken);
if (token == null ||
!string.Equals(await Store.GetReferenceIdAsync(token, cancellationToken), identifier, StringComparison.Ordinal))
{
return null;
}
return token;
}
/// <summary>
@ -262,7 +274,7 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified subject.
/// </returns>
public virtual Task<ImmutableArray<TToken>> FindBySubjectAsync(
public virtual async Task<ImmutableArray<TToken>> FindBySubjectAsync(
[NotNull] string subject, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(subject))
@ -270,7 +282,29 @@ namespace OpenIddict.Core
throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
}
return Store.FindBySubjectAsync(subject, cancellationToken);
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
var tokens = await Store.FindBySubjectAsync(subject, cancellationToken);
if (tokens.IsEmpty)
{
return ImmutableArray.Create<TToken>();
}
var builder = ImmutableArray.CreateBuilder<TToken>(tokens.Length);
foreach (var token in tokens)
{
if (string.Equals(await Store.GetSubjectAsync(token, cancellationToken), subject, StringComparison.Ordinal))
{
builder.Add(token);
}
}
return builder.Count == builder.Capacity ?
builder.MoveToImmutable() :
builder.ToImmutable();
}
/// <summary>
@ -821,46 +855,52 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(token));
}
var results = ImmutableArray.CreateBuilder<ValidationResult>();
var builder = ImmutableArray.CreateBuilder<ValidationResult>();
// If a reference identifier was associated with the token,
// ensure it's not already used for a different token.
var identifier = await Store.GetReferenceIdAsync(token, cancellationToken);
if (!string.IsNullOrEmpty(identifier))
{
// Note: depending on the database/table/query collation used by the store, a reference token
// whose identifier doesn't exactly match the specified value may be returned (e.g because
// the casing is different). To avoid issues when the reference identifier is part of an index
// using the same collation, an error is added even if the two identifiers don't exactly match.
var other = await Store.FindByReferenceIdAsync(identifier, cancellationToken);
if (other != null && !string.Equals(
await Store.GetIdAsync(other, cancellationToken),
await Store.GetIdAsync(token, cancellationToken), StringComparison.Ordinal))
{
results.Add(new ValidationResult("A token with the same reference identifier already exists."));
builder.Add(new ValidationResult("A token with the same reference identifier already exists."));
}
}
var type = await Store.GetTokenTypeAsync(token, cancellationToken);
if (string.IsNullOrEmpty(type))
{
results.Add(new ValidationResult("The token type cannot be null or empty."));
builder.Add(new ValidationResult("The token type cannot be null or empty."));
}
else if (!string.Equals(type, OpenIddictConstants.TokenTypes.AccessToken, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(type, OpenIddictConstants.TokenTypes.AuthorizationCode, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(type, OpenIddictConstants.TokenTypes.RefreshToken, StringComparison.OrdinalIgnoreCase))
{
results.Add(new ValidationResult("The specified token type is not supported by the default token manager."));
builder.Add(new ValidationResult("The specified token type is not supported by the default token manager."));
}
if (string.IsNullOrEmpty(await Store.GetStatusAsync(token, cancellationToken)))
{
results.Add(new ValidationResult("The status cannot be null or empty."));
builder.Add(new ValidationResult("The status cannot be null or empty."));
}
if (string.IsNullOrEmpty(await Store.GetSubjectAsync(token, cancellationToken)))
{
results.Add(new ValidationResult("The subject cannot be null or empty."));
builder.Add(new ValidationResult("The subject cannot be null or empty."));
}
return results.ToImmutable();
return builder.Count == builder.Capacity ?
builder.MoveToImmutable() :
builder.ToImmutable();
}
/// <summary>

8
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs

@ -346,7 +346,9 @@ namespace OpenIddict.EntityFrameworkCore
}
}
return builder.ToImmutable();
return builder.Count == builder.Capacity ?
builder.MoveToImmutable() :
builder.ToImmutable();
}
/// <summary>
@ -399,7 +401,9 @@ namespace OpenIddict.EntityFrameworkCore
}
}
return builder.ToImmutable();
return builder.Count == builder.Capacity ?
builder.MoveToImmutable() :
builder.ToImmutable();
}
/// <summary>

8
src/OpenIddict.Stores/Stores/OpenIddictApplicationStore.cs

@ -184,7 +184,9 @@ namespace OpenIddict.Stores
}
}
return builder.ToImmutable();
return builder.Count == builder.Capacity ?
builder.MoveToImmutable() :
builder.ToImmutable();
}
/// <summary>
@ -230,7 +232,9 @@ namespace OpenIddict.Stores
}
}
return builder.ToImmutable();
return builder.Count == builder.Capacity ?
builder.MoveToImmutable() :
builder.ToImmutable();
}
/// <summary>

Loading…
Cancel
Save