diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
index c9869f39..c52bf4b8 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
+++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
@@ -230,14 +230,25 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
///
- public virtual Task FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default)
+ public virtual async Task 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;
}
///
@@ -249,7 +260,7 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified post_logout_redirect_uri.
///
- public virtual Task> FindByPostLogoutRedirectUriAsync(
+ public virtual async Task> 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();
+ }
+
+ var builder = ImmutableArray.CreateBuilder(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();
}
///
@@ -269,7 +306,7 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified redirect_uri.
///
- public virtual Task> FindByRedirectUriAsync(
+ public virtual async Task> 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();
+ }
+
+ var builder = ImmutableArray.CreateBuilder(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();
}
///
@@ -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();
+ var builder = ImmutableArray.CreateBuilder();
+ // 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();
}
///
@@ -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}' " +
diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
index c944105c..2c6d9116 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
+++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
@@ -222,7 +222,7 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the subject/client.
///
- public virtual Task> FindAsync(
+ public virtual async Task> 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();
+ }
+
+ var builder = ImmutableArray.CreateBuilder(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();
}
///
@@ -249,7 +271,7 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the criteria.
///
- public virtual Task> FindAsync(
+ public virtual async Task> 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();
+ }
+
+ var builder = ImmutableArray.CreateBuilder(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();
}
///
@@ -283,7 +327,7 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the criteria.
///
- public virtual Task> FindAsync(
+ public virtual async Task> 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();
+ }
+
+ var builder = ImmutableArray.CreateBuilder(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();
}
///
@@ -328,17 +394,25 @@ namespace OpenIddict.Core
[NotNull] string status, [NotNull] string type,
ImmutableArray scopes, CancellationToken cancellationToken = default)
{
- var authorizations = ImmutableArray.CreateBuilder();
+ var authorizations = await FindAsync(subject, client, status, type, cancellationToken);
+ if (authorizations.IsEmpty)
+ {
+ return ImmutableArray.Create();
+ }
+
+ var builder = ImmutableArray.CreateBuilder(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();
}
///
@@ -369,7 +443,7 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the specified subject.
///
- public virtual Task> FindBySubjectAsync(
+ public virtual async Task> 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();
+ }
+
+ var builder = ImmutableArray.CreateBuilder(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();
}
///
@@ -834,28 +930,28 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(authorization));
}
- var results = ImmutableArray.CreateBuilder();
+ var builder = ImmutableArray.CreateBuilder();
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();
}
///
diff --git a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs
index ec4015d4..de70b00c 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs
+++ b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs
@@ -172,14 +172,24 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the specified name.
///
- public virtual Task FindByNameAsync([NotNull] string name, CancellationToken cancellationToken = default)
+ public virtual async Task 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;
}
///
@@ -191,7 +201,7 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the scopes corresponding to the specified names.
///
- public virtual Task> FindByNamesAsync(
+ public virtual async Task> FindByNamesAsync(
ImmutableArray 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();
+ }
+
+ var builder = ImmutableArray.CreateBuilder(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();
}
///
@@ -499,32 +531,39 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(scope));
}
- var results = ImmutableArray.CreateBuilder();
+ var builder = ImmutableArray.CreateBuilder();
+ // 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();
}
///
diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
index 2ea62973..0b2a97f3 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
+++ b/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;
}
///
@@ -262,7 +274,7 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified subject.
///
- public virtual Task> FindBySubjectAsync(
+ public virtual async Task> 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();
+ }
+
+ var builder = ImmutableArray.CreateBuilder(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();
}
///
@@ -821,46 +855,52 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(token));
}
- var results = ImmutableArray.CreateBuilder();
+ var builder = ImmutableArray.CreateBuilder();
// 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();
}
///
diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs
index c378cfb8..efc2f7e6 100644
--- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs
+++ b/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();
}
///
@@ -399,7 +401,9 @@ namespace OpenIddict.EntityFrameworkCore
}
}
- return builder.ToImmutable();
+ return builder.Count == builder.Capacity ?
+ builder.MoveToImmutable() :
+ builder.ToImmutable();
}
///
diff --git a/src/OpenIddict.Stores/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.Stores/Stores/OpenIddictApplicationStore.cs
index 9a5b2e1a..c3f4ebb6 100644
--- a/src/OpenIddict.Stores/Stores/OpenIddictApplicationStore.cs
+++ b/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();
}
///
@@ -230,7 +232,9 @@ namespace OpenIddict.Stores
}
}
- return builder.ToImmutable();
+ return builder.Count == builder.Capacity ?
+ builder.MoveToImmutable() :
+ builder.ToImmutable();
}
///