diff --git a/src/modules/Elsa.Identity/Extensions/ApplicationProviderExtensions.cs b/src/modules/Elsa.Identity/Extensions/ApplicationProviderExtensions.cs
index 5de5515cf..a99a7e3cf 100644
--- a/src/modules/Elsa.Identity/Extensions/ApplicationProviderExtensions.cs
+++ b/src/modules/Elsa.Identity/Extensions/ApplicationProviderExtensions.cs
@@ -15,15 +15,17 @@ public static class ApplicationProviderExtensions
///
/// The user provider.
/// The client ID to search by.
+ /// Whether to bypass tenant scoping when resolving the application.
/// The cancellation token.
/// The application with the specified client ID.
- public static async Task FindByClientIdAsync(this IApplicationProvider applicationProvider, string clientId, CancellationToken cancellationToken = default)
+ public static async Task FindByClientIdAsync(this IApplicationProvider applicationProvider, string clientId, bool tenantAgnostic = false, CancellationToken cancellationToken = default)
{
var filter = new ApplicationFilter
{
- ClientId = clientId
+ ClientId = clientId,
+ TenantAgnostic = tenantAgnostic
};
return await applicationProvider.FindAsync(filter, cancellationToken);
}
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Identity/Models/ApplicationFilter.cs b/src/modules/Elsa.Identity/Models/ApplicationFilter.cs
index 9de2b4ad4..c19d7c5f2 100644
--- a/src/modules/Elsa.Identity/Models/ApplicationFilter.cs
+++ b/src/modules/Elsa.Identity/Models/ApplicationFilter.cs
@@ -21,6 +21,11 @@ public class ApplicationFilter
/// Gets or sets the application name to filter for.
///
public string? Name { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to ignore tenant scoping when querying the application store.
+ ///
+ public bool TenantAgnostic { get; set; }
///
/// Applies the filter to the specified queryable.
@@ -36,4 +41,4 @@ public class ApplicationFilter
return queryable;
}
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Identity/Services/DefaultApplicationCredentialsValidator.cs b/src/modules/Elsa.Identity/Services/DefaultApplicationCredentialsValidator.cs
index 6ddfe646f..2c19e2198 100644
--- a/src/modules/Elsa.Identity/Services/DefaultApplicationCredentialsValidator.cs
+++ b/src/modules/Elsa.Identity/Services/DefaultApplicationCredentialsValidator.cs
@@ -50,7 +50,7 @@ public class DefaultApplicationCredentialsValidator : IApplicationCredentialsVal
return null;
var clientId = _apiKeyParser.Parse(apiKey);
- var application = await _applicationProvider.FindByClientIdAsync(clientId, cancellationToken);
+ var application = await _applicationProvider.FindByClientIdAsync(clientId, tenantAgnostic: true, cancellationToken);
if(application == null)
return null;
diff --git a/src/modules/Elsa.Persistence.EFCore/Modules/Identity/ApplicationStore.cs b/src/modules/Elsa.Persistence.EFCore/Modules/Identity/ApplicationStore.cs
index 48732d8ac..69dbaf2ab 100644
--- a/src/modules/Elsa.Persistence.EFCore/Modules/Identity/ApplicationStore.cs
+++ b/src/modules/Elsa.Persistence.EFCore/Modules/Identity/ApplicationStore.cs
@@ -34,8 +34,8 @@ public class EFCoreApplicationStore : IApplicationStore
///
public async Task FindAsync(ApplicationFilter filter, CancellationToken cancellationToken = default)
{
- return await _applicationStore.FindAsync(query => Filter(query, filter), cancellationToken);
+ return await _applicationStore.FindAsync(query => Filter(query, filter), filter.TenantAgnostic, cancellationToken);
}
private static IQueryable Filter(IQueryable query, ApplicationFilter filter) => filter.Apply(query);
-}
\ No newline at end of file
+}
diff --git a/test/unit/Elsa.Identity.UnitTests/Services/DefaultSecretHasherTests.cs b/test/unit/Elsa.Identity.UnitTests/Services/DefaultSecretHasherTests.cs
index d47e04d93..c343b3441 100644
--- a/test/unit/Elsa.Identity.UnitTests/Services/DefaultSecretHasherTests.cs
+++ b/test/unit/Elsa.Identity.UnitTests/Services/DefaultSecretHasherTests.cs
@@ -265,6 +265,34 @@ public class DefaultSecretHasherTests
Assert.Equal(encodedLegacyHash, application.HashedApiKey);
}
+ [Fact]
+ public async Task ValidateAsync_UsesTenantAgnosticLookupForApplications()
+ {
+ var apiKeyGenerator = new DefaultApiKeyGeneratorAndParser();
+ var apiKey = apiKeyGenerator.Generate("client-1");
+ var hasher = new DefaultSecretHasher();
+ var hashedApiKey = hasher.HashSecret(apiKey);
+ var application = new Application
+ {
+ Id = "app-1",
+ ClientId = "client-1",
+ Name = "Client 1",
+ HashedApiKey = hashedApiKey.EncodeSecret(),
+ HashedApiKeySalt = hashedApiKey.EncodeSalt(),
+ HashedClientSecret = "",
+ HashedClientSecretSalt = ""
+ };
+ var applicationProvider = new RecordingApplicationProvider(application);
+ var validator = new DefaultApplicationCredentialsValidator(apiKeyGenerator, applicationProvider, hasher);
+
+ var validatedApplication = await validator.ValidateAsync(apiKey);
+
+ Assert.Same(application, validatedApplication);
+ Assert.NotNull(applicationProvider.LastFilter);
+ Assert.True(applicationProvider.LastFilter!.TenantAgnostic);
+ Assert.Equal("client-1", applicationProvider.LastFilter.ClientId);
+ }
+
private static HashedSecret CreateLegacyHash(string secret)
{
var salt = RandomNumberGenerator.GetBytes(32);
@@ -370,6 +398,19 @@ public class DefaultSecretHasherTests
}
}
+ private sealed class RecordingApplicationProvider(Application application) : IApplicationProvider
+ {
+ private readonly Application _application = application;
+ public ApplicationFilter? LastFilter { get; private set; }
+
+ public Task FindAsync(ApplicationFilter filter, CancellationToken cancellationToken = default)
+ {
+ LastFilter = filter;
+ var application = filter.Apply(new[] { _application }.AsQueryable()).FirstOrDefault();
+ return Task.FromResult(application);
+ }
+ }
+
private sealed class CultureScope : IDisposable
{
private readonly CultureInfo _currentCulture = CultureInfo.CurrentCulture;