From e471d9e7fa0681b3e0b1afac29da9504af5e76dc Mon Sep 17 00:00:00 2001 From: Padmanabh Gupta Date: Mon, 13 May 2024 01:56:46 +0530 Subject: [PATCH 01/11] - Added option to allow AAD auth with ACL - Currently ACL auth by default does username and password validation with ACL entries. We compose it with an IAuthenticator instance to inject authentication behavior of username and password and then just validate the permissions against the ACL list of the user. This approach is more favorable as it minimizes changes and avoids redundant code needed to combine behaviors of AclAuthenticator and AADAuthenticator. Rather than inheriting these behavior, we compose AclAuthenticator with an IAuthenticator. Testing: There were no AAD tests. Have added a basic test to validate AAD + ACL and that it works with cluster auth. --- libs/host/Configuration/Options.cs | 5 ++ .../Auth/Aad/IssuerSigningTokenProvider.cs | 17 +++- libs/server/Auth/AuthenticationSettings.cs | 31 +++++++- libs/server/Auth/GarnetACLAuthenticator.cs | 22 +++++- libs/server/Auth/GarnetAadAuthenticator.cs | 47 +++++++++-- .../ClusterAadAuthTests.cs | 77 +++++++++++++++++++ .../Garnet.test.cluster/ClusterTestContext.cs | 7 +- test/Garnet.test.cluster/JwtTokenGenerator.cs | 41 ++++++++++ test/Garnet.test/TestUtils.cs | 12 ++- 9 files changed, 241 insertions(+), 18 deletions(-) create mode 100644 test/Garnet.test.cluster/ClusterAadAuthTests.cs create mode 100644 test/Garnet.test.cluster/JwtTokenGenerator.cs diff --git a/libs/host/Configuration/Options.cs b/libs/host/Configuration/Options.cs index f684e38c44..df65715b85 100644 --- a/libs/host/Configuration/Options.cs +++ b/libs/host/Configuration/Options.cs @@ -624,6 +624,11 @@ private IAuthenticationSettings GetAuthenticationSettings(ILogger logger = null) return new AadAuthenticationSettings(AuthorizedAadApplicationIds?.Split(','), AadAudiences?.Split(','), AadIssuers?.Split(','), IssuerSigningTokenProvider.Create(AadAuthority, logger)); case GarnetAuthenticationMode.ACL: return new AclAuthenticationSettings(AclFile, Password); + case GarnetAuthenticationMode.AclWithAad: + var aadAuthSettings = new AadAuthenticationSettings(AuthorizedAadApplicationIds?.Split(','), AadAudiences?.Split(','), AadIssuers?.Split(','), IssuerSigningTokenProvider.Create(AadAuthority, logger)); + aadAuthSettings = aadAuthSettings.WithUsernameValidation(); + return new AclAuthenticationSettings(AclFile, Password, aadAuthSettings); + default: logger?.LogError("Unsupported authentication mode: {mode}", AuthenticationMode); throw new Exception($"Authentication mode {AuthenticationMode} is not supported."); diff --git a/libs/server/Auth/Aad/IssuerSigningTokenProvider.cs b/libs/server/Auth/Aad/IssuerSigningTokenProvider.cs index 944865c90e..d4fac74f51 100644 --- a/libs/server/Auth/Aad/IssuerSigningTokenProvider.cs +++ b/libs/server/Auth/Aad/IssuerSigningTokenProvider.cs @@ -31,15 +31,17 @@ public class IssuerSigningTokenProvider : IDisposable private readonly ILogger _logger; - private IssuerSigningTokenProvider(string authority, IReadOnlyCollection signingTokens, ILogger logger) + private IssuerSigningTokenProvider(string authority, IReadOnlyCollection signingTokens, ILogger logger, bool refreshTokens = true) { _authority = authority; - _refreshTimer = new Timer(RefreshSigningTokens, null, TimeSpan.Zero, TimeSpan.FromDays(1)); + if (refreshTokens) + _refreshTimer = new Timer(RefreshSigningTokens, null, TimeSpan.Zero, TimeSpan.FromDays(1)); _signingTokens = signingTokens; _logger = logger; } + private void RefreshSigningTokens(object _) { try @@ -105,5 +107,16 @@ public static IssuerSigningTokenProvider Create(string authority, ILogger logger var signingTokens = RetrieveSigningTokens(authority); return new IssuerSigningTokenProvider(authority, signingTokens, logger); } + + /// + /// [Used for testing] + /// Creates an instance of IssuerSigningTokenProvider without refresh of tokens and hence this doesn't need authority. + /// + /// + /// The logger + public static IssuerSigningTokenProvider Create(IReadOnlyCollection signingToken, ILogger logger) + { + return new IssuerSigningTokenProvider(string.Empty, signingToken, logger, refreshTokens: false); + } } } \ No newline at end of file diff --git a/libs/server/Auth/AuthenticationSettings.cs b/libs/server/Auth/AuthenticationSettings.cs index 237005d881..24f7b8102a 100644 --- a/libs/server/Auth/AuthenticationSettings.cs +++ b/libs/server/Auth/AuthenticationSettings.cs @@ -31,7 +31,12 @@ public enum GarnetAuthenticationMode /// /// ACL - Garnet validates new connections and commands against configured ACL users and access rules. /// - ACL + ACL, + + /// + /// ACL mode using Aad token instead of password. Here username is expected to be objectId and token will be validated for claims. + /// + AclWithAad } /// @@ -116,6 +121,7 @@ public class AadAuthenticationSettings : IAuthenticationSettings private readonly IReadOnlyCollection _audiences; private readonly IReadOnlyCollection _issuers; private IssuerSigningTokenProvider _signingTokenProvider; + private bool _validateUsername; private bool _disposed; /// @@ -153,13 +159,27 @@ public AadAuthenticationSettings(string[] authorizedAppIds, string[] audiences, _signingTokenProvider = signingTokenProvier; } + public AadAuthenticationSettings WithUsernameValidation() + { + _validateUsername = true; + return this; + } + /// /// Creates an AAD auth authenticator /// /// The main store the authenticator will be associated with. public IGarnetAuthenticator CreateAuthenticator(StoreWrapper storeWrapper) { - return new GarnetAadAuthenticator(_authorizedAppIds, _audiences, _issuers, _signingTokenProvider, storeWrapper.logger); + var config = new AadValidationConfig + { + _audiences = _audiences, + _authorizedAppIds = _authorizedAppIds, + _issuers = _issuers, + _signingTokenProvider = _signingTokenProvider, + ValidateUsername = _validateUsername, + }; + return new GarnetAadAuthenticator(config, storeWrapper.logger); } /// @@ -205,15 +225,18 @@ public class AclAuthenticationSettings : IAuthenticationSettings /// public readonly string DefaultPassword; + private IAuthenticationSettings settings; + /// /// Creates and initializes new ACL authentication settings /// /// Location of the ACL configuration file /// Optional default password, if not defined through aclConfigurationFile - public AclAuthenticationSettings(string aclConfigurationFile, string defaultPassword = "") + public AclAuthenticationSettings(string aclConfigurationFile, string defaultPassword = "", IAuthenticationSettings settings = null) { AclConfigurationFile = aclConfigurationFile; DefaultPassword = defaultPassword; + this.settings = settings; } /// @@ -222,7 +245,7 @@ public AclAuthenticationSettings(string aclConfigurationFile, string defaultPass /// The main store the authenticator will be associated with. public IGarnetAuthenticator CreateAuthenticator(StoreWrapper storeWrapper) { - return new GarnetACLAuthenticator(storeWrapper.accessControlList, storeWrapper.logger); + return new GarnetACLAuthenticator(storeWrapper.accessControlList, storeWrapper.logger, settings?.CreateAuthenticator(storeWrapper)); } /// diff --git a/libs/server/Auth/GarnetACLAuthenticator.cs b/libs/server/Auth/GarnetACLAuthenticator.cs index 70089f75a1..2c0d0f891e 100644 --- a/libs/server/Auth/GarnetACLAuthenticator.cs +++ b/libs/server/Auth/GarnetACLAuthenticator.cs @@ -25,15 +25,19 @@ class GarnetACLAuthenticator : IGarnetAuthenticator /// User _user = null; + + private IGarnetAuthenticator _authenticator; + /// /// Initializes a new ACLAuthenticator instance. /// /// Access control list to authenticate against /// The logger to use - public GarnetACLAuthenticator(AccessControlList accessControlList, ILogger logger) + public GarnetACLAuthenticator(AccessControlList accessControlList, ILogger logger, IGarnetAuthenticator wrapperAuthenticator = null) { _acl = accessControlList; _logger = logger; + _authenticator = wrapperAuthenticator; } /// @@ -65,6 +69,22 @@ public bool Authenticate(ReadOnlySpan password, ReadOnlySpan usernam // Check if user exists and set default user if username is unspecified string uname = Encoding.ASCII.GetString(username); User user = string.IsNullOrEmpty(uname) ? _acl.GetDefaultUser() : _acl.GetUser(uname); + + if(user == null) + { + return false; + } + + // Use injected authenticator if configured. + if (_authenticator != null) + { + if (user.IsEnabled && _authenticator.Authenticate(password, username)) + { + _user = user; + successful = true; + } + return successful; + } // Try to authenticate user ACLPassword passwordHash = ACLPassword.ACLPasswordFromString(Encoding.ASCII.GetString(password)); diff --git a/libs/server/Auth/GarnetAadAuthenticator.cs b/libs/server/Auth/GarnetAadAuthenticator.cs index 0f26601520..c52d4b7c37 100644 --- a/libs/server/Auth/GarnetAadAuthenticator.cs +++ b/libs/server/Auth/GarnetAadAuthenticator.cs @@ -13,6 +13,18 @@ namespace Garnet.server.Auth { + internal class AadValidationConfig + { + internal IReadOnlyCollection _authorizedAppIds { get; set; } + internal IReadOnlyCollection _audiences { get; set; } + internal IReadOnlyCollection _issuers { get; set; } + + internal IssuerSigningTokenProvider _signingTokenProvider { get; set; } + + internal bool ValidateUsername { get; set; } + + } + class GarnetAadAuthenticator : IGarnetAuthenticator { private static JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler(); @@ -20,6 +32,7 @@ class GarnetAadAuthenticator : IGarnetAuthenticator private const string _appIdAcrClaim = "appidacr"; private const string _scopeClaim = "http://schemas.microsoft.com/identity/claims/scope"; private const string _appIdClaim = "appid"; + private const string _oidClaim = "http://schemas.microsoft.com/identity/claims/objectidentifier"; public bool IsAuthenticated => IsAuthorized(); @@ -34,6 +47,7 @@ class GarnetAadAuthenticator : IGarnetAuthenticator private readonly IReadOnlyCollection _audiences; private readonly IReadOnlyCollection _issuers; private readonly IssuerSigningTokenProvider _signingTokenProvider; + private readonly bool ValidateUsername; private readonly ILogger _logger; @@ -48,6 +62,17 @@ public GarnetAadAuthenticator( _signingTokenProvider = signingTokenProvider; _audiences = audiences; _issuers = issuers; + ValidateUsername = false; + _logger = logger; + } + + internal GarnetAadAuthenticator(AadValidationConfig aadValidationConfig, ILogger logger) + { + _authorizedAppIds = aadValidationConfig._authorizedAppIds; + _signingTokenProvider = aadValidationConfig._signingTokenProvider; + _audiences = aadValidationConfig._audiences; + _issuers = aadValidationConfig._issuers; + ValidateUsername = aadValidationConfig.ValidateUsername; _logger = logger; } @@ -62,13 +87,17 @@ public bool Authenticate(ReadOnlySpan password, ReadOnlySpan usernam ValidAudiences = _audiences, IssuerSigningKeys = _signingTokenProvider.SigningTokens }; - - var identity = _tokenHandler.ValidateToken(Encoding.UTF8.GetString(password), parameters, out var token); + var passwordStr = Encoding.UTF8.GetString(password); + if (string.IsNullOrWhiteSpace(passwordStr)) + { + return false; + } + var identity = _tokenHandler.ValidateToken(passwordStr, parameters, out var token); _validFrom = token.ValidFrom; _validateTo = token.ValidTo; - _authorized = IsIdentityAuthorized(identity); + _authorized = IsIdentityAuthorized(identity, username); _logger?.LogInformation($"Authentication successful. Token valid from {_validFrom} to {_validateTo}"); return IsAuthorized(); @@ -83,13 +112,14 @@ public bool Authenticate(ReadOnlySpan password, ReadOnlySpan usernam } } - private bool IsIdentityAuthorized(ClaimsPrincipal identity) + private bool IsIdentityAuthorized(ClaimsPrincipal identity, ReadOnlySpan userName) { var claims = identity.Claims .GroupBy(claim => claim.Type) .ToDictionary(group => group.Key, group => string.Join(',', group.Select(c => c.Value)), StringComparer.OrdinalIgnoreCase); - return IsApplicationPrincipal(claims) && IsApplicationAuthorized(claims); + bool isValid = IsApplicationPrincipal(claims) && IsApplicationAuthorized(claims); + return !ValidateUsername ? isValid : ValidateUsername && IsUserNameAuthorized(claims, userName); } private bool IsApplicationAuthorized(IDictionary claims) @@ -97,6 +127,13 @@ private bool IsApplicationAuthorized(IDictionary claims) return claims.TryGetValue(_appIdClaim, out var appId) && _authorizedAppIds.Contains(appId); } + private bool IsUserNameAuthorized(IDictionary claims, ReadOnlySpan userName) + { + var userNameStr = Encoding.UTF8.GetString(userName); + return claims.TryGetValue(_oidClaim, out var oid) && userNameStr.Equals(oid, StringComparison.InvariantCultureIgnoreCase); + } + + private bool IsAuthorized() { var now = DateTime.UtcNow; diff --git a/test/Garnet.test.cluster/ClusterAadAuthTests.cs b/test/Garnet.test.cluster/ClusterAadAuthTests.cs new file mode 100644 index 0000000000..97a411bc2e --- /dev/null +++ b/test/Garnet.test.cluster/ClusterAadAuthTests.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using Garnet.server.Auth; +using Garnet.server.Auth.Aad; +using Microsoft.IdentityModel.Tokens; +using NUnit.Framework; + +namespace Garnet.test.cluster +{ + [TestFixture] + [NonParallelizable] + class ClusterAadAuthTests + { + ClusterTestContext context; + + readonly HashSet monitorTests = []; + + [SetUp] + public void Setup() + { + context = new ClusterTestContext(); + context.Setup(monitorTests); + } + + [TearDown] + public void TearDown() + { + context.TearDown(); + } + + [Test, Order(1)] + [Category("CLUSTER-AUTH"), Timeout(60000)] + public void ValidateClusterAuth() + { + var nodes = 2; + var audience = Guid.NewGuid().ToString(); + JwtTokenGenerator tokenGenerator = new JwtTokenGenerator(audience); + + var appId = Guid.NewGuid().ToString(); + var objId = Guid.NewGuid().ToString(); + var tokenClaims = new List + { + new Claim("appidacr","1"), + new Claim("appid", appId), + new Claim("http://schemas.microsoft.com/identity/claims/objectidentifier",objId), + }; + var authSettings = new AadAuthenticationSettings([appId], [audience], [audience], IssuerSigningTokenProvider.Create(new List { tokenGenerator.SecurityKey }, context.logger)); + authSettings = authSettings.WithUsernameValidation(); + var token = tokenGenerator.CreateToken(tokenClaims, DateTime.Now.AddMinutes(10)); + // Generate default ACL file + + var userCredential = new ServerCredential { user = objId, IsAdmin= true, IsClearText = true }; + var clientCredentials = new ServerCredential { user = objId, password = token }; + context.GenerateCredentials([userCredential]); + context.CreateInstances(nodes, useAcl: true, clusterCreds: clientCredentials, authenticationSettings: authSettings); + + + context.CreateConnection(useTLS: false, clientCreds: clientCredentials); + + for (int i = 0; i < nodes; i++) + { + context.clusterTestUtils.Authenticate(i, clientCredentials.user, clientCredentials.password, context.logger); + context.clusterTestUtils.Meet(i, (i + 1) % nodes, context.logger); + var ex = Assert.Throws(() => context.clusterTestUtils.Authenticate(i, "randomUserId", clientCredentials.password, context.logger)); + Assert.AreEqual("WRONGPASS Invalid username/password combination", ex.Message); + } + + } + } +} diff --git a/test/Garnet.test.cluster/ClusterTestContext.cs b/test/Garnet.test.cluster/ClusterTestContext.cs index a351ff53ef..6579ed26c5 100644 --- a/test/Garnet.test.cluster/ClusterTestContext.cs +++ b/test/Garnet.test.cluster/ClusterTestContext.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Garnet.server.Auth; using Microsoft.Extensions.Logging; using NUnit.Framework; using StackExchange.Redis; @@ -102,7 +103,8 @@ public void CreateInstances( bool useTLS = false, bool useAcl = false, X509CertificateCollection certificates = null, - ServerCredential clusterCreds = new ServerCredential()) + ServerCredential clusterCreds = new ServerCredential(), + AadAuthenticationSettings authenticationSettings = null) { endpoints = TestUtils.GetEndPoints(shards, 7000); nodes = TestUtils.CreateGarnetCluster( @@ -131,7 +133,8 @@ public void CreateInstances( aclFile: credManager.aclFilePath, authUsername: clusterCreds.user, authPassword: clusterCreds.password, - certificates: certificates); + certificates: certificates, + authenticationSettings: authenticationSettings); foreach (var node in nodes) node.Start(); diff --git a/test/Garnet.test.cluster/JwtTokenGenerator.cs b/test/Garnet.test.cluster/JwtTokenGenerator.cs new file mode 100644 index 0000000000..59e90e05a2 --- /dev/null +++ b/test/Garnet.test.cluster/JwtTokenGenerator.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Tokens; + +namespace Garnet.test.cluster +{ + internal class JwtTokenGenerator + { + private readonly string Issuer; + + // Our random signing key - used to sign and validate the tokens + public SecurityKey SecurityKey { get; } + + // the signing credentials used by the token handler to sign tokens + public SigningCredentials SigningCredentials { get; } + + // the token handler we'll use to actually issue tokens + public readonly JwtSecurityTokenHandler JwtSecurityTokenHandler = new(); + + internal JwtTokenGenerator(string issuer) + { + Issuer = issuer; + SecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("this is my custom Secret key for authentication")); + SigningCredentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256); + } + + internal string CreateToken(List claims, DateTime expiryTime) + { + return JwtSecurityTokenHandler.WriteToken(new JwtSecurityToken(Issuer, Issuer, claims, expires: expiryTime, signingCredentials: SigningCredentials)); + } + + } +} diff --git a/test/Garnet.test/TestUtils.cs b/test/Garnet.test/TestUtils.cs index 7b04ed288a..4e675f8b6a 100644 --- a/test/Garnet.test/TestUtils.cs +++ b/test/Garnet.test/TestUtils.cs @@ -266,7 +266,8 @@ public static GarnetServer[] CreateGarnetCluster( bool useAcl = false, // NOTE: Temporary until ACL is enforced as default string aclFile = null, X509CertificateCollection certificates = null, - ILoggerFactory loggerFactory = null) + ILoggerFactory loggerFactory = null, + AadAuthenticationSettings authenticationSettings = null) { if (UseAzureStorage) IgnoreIfNotRunningAzureTests(); @@ -304,7 +305,8 @@ public static GarnetServer[] CreateGarnetCluster( useAcl: useAcl, aclFile: aclFile, certificates: certificates, - logger: loggerFactory?.CreateLogger("GarnetServer")); + logger: loggerFactory?.CreateLogger("GarnetServer"), + aadAuthenticationSettings: authenticationSettings); Assert.IsNotNull(opts); int iter = 0; @@ -348,7 +350,8 @@ public static GarnetServerOptions GetGarnetServerOptions( bool useAcl = false, // NOTE: Temporary until ACL is enforced as default string aclFile = null, X509CertificateCollection certificates = null, - ILogger logger = null) + ILogger logger = null, + AadAuthenticationSettings aadAuthenticationSettings = null) { if (UseAzureStorage) IgnoreIfNotRunningAzureTests(); @@ -365,7 +368,8 @@ public static GarnetServerOptions GetGarnetServerOptions( IAuthenticationSettings authenticationSettings = null; if (useAcl) { - authenticationSettings = new AclAuthenticationSettings(aclFile, authPassword); + + authenticationSettings = new AclAuthenticationSettings(aclFile, authPassword, aadAuthenticationSettings); } else if (authPassword != null) { From 1accb31c302d09832be7ca300cd6dbac46e8c073 Mon Sep 17 00:00:00 2001 From: Padmanabh Gupta Date: Mon, 13 May 2024 10:24:11 +0530 Subject: [PATCH 02/11] formatting fixes --- libs/host/Configuration/Options.cs | 2 +- .../server/Auth/Aad/IssuerSigningTokenProvider.cs | 4 +++- libs/server/Auth/AuthenticationSettings.cs | 5 ++--- libs/server/Auth/GarnetACLAuthenticator.cs | 10 +++++----- libs/server/Auth/GarnetAadAuthenticator.cs | 15 +++------------ test/Garnet.test.cluster/ClusterAadAuthTests.cs | 5 ++--- test/Garnet.test.cluster/JwtTokenGenerator.cs | 8 ++++---- 7 files changed, 20 insertions(+), 29 deletions(-) diff --git a/libs/host/Configuration/Options.cs b/libs/host/Configuration/Options.cs index df65715b85..56b89be795 100644 --- a/libs/host/Configuration/Options.cs +++ b/libs/host/Configuration/Options.cs @@ -626,7 +626,7 @@ private IAuthenticationSettings GetAuthenticationSettings(ILogger logger = null) return new AclAuthenticationSettings(AclFile, Password); case GarnetAuthenticationMode.AclWithAad: var aadAuthSettings = new AadAuthenticationSettings(AuthorizedAadApplicationIds?.Split(','), AadAudiences?.Split(','), AadIssuers?.Split(','), IssuerSigningTokenProvider.Create(AadAuthority, logger)); - aadAuthSettings = aadAuthSettings.WithUsernameValidation(); + aadAuthSettings = aadAuthSettings.WithUsernameValidation(); return new AclAuthenticationSettings(AclFile, Password, aadAuthSettings); default: diff --git a/libs/server/Auth/Aad/IssuerSigningTokenProvider.cs b/libs/server/Auth/Aad/IssuerSigningTokenProvider.cs index d4fac74f51..2553b543e6 100644 --- a/libs/server/Auth/Aad/IssuerSigningTokenProvider.cs +++ b/libs/server/Auth/Aad/IssuerSigningTokenProvider.cs @@ -34,8 +34,10 @@ public class IssuerSigningTokenProvider : IDisposable private IssuerSigningTokenProvider(string authority, IReadOnlyCollection signingTokens, ILogger logger, bool refreshTokens = true) { _authority = authority; - if (refreshTokens) + if (refreshTokens) + { _refreshTimer = new Timer(RefreshSigningTokens, null, TimeSpan.Zero, TimeSpan.FromDays(1)); + } _signingTokens = signingTokens; _logger = logger; diff --git a/libs/server/Auth/AuthenticationSettings.cs b/libs/server/Auth/AuthenticationSettings.cs index 24f7b8102a..5f2c1b6844 100644 --- a/libs/server/Auth/AuthenticationSettings.cs +++ b/libs/server/Auth/AuthenticationSettings.cs @@ -32,9 +32,8 @@ public enum GarnetAuthenticationMode /// ACL - Garnet validates new connections and commands against configured ACL users and access rules. /// ACL, - /// - /// ACL mode using Aad token instead of password. Here username is expected to be objectId and token will be validated for claims. + /// ACL mode using Aad token instead of password. Here username is expected to be ObjectId and token will be validated for claims. /// AclWithAad } @@ -232,7 +231,7 @@ public class AclAuthenticationSettings : IAuthenticationSettings /// /// Location of the ACL configuration file /// Optional default password, if not defined through aclConfigurationFile - public AclAuthenticationSettings(string aclConfigurationFile, string defaultPassword = "", IAuthenticationSettings settings = null) + public AclAuthenticationSettings(string aclConfigurationFile, string defaultPassword = "", IAuthenticationSettings settings = null) { AclConfigurationFile = aclConfigurationFile; DefaultPassword = defaultPassword; diff --git a/libs/server/Auth/GarnetACLAuthenticator.cs b/libs/server/Auth/GarnetACLAuthenticator.cs index 2c0d0f891e..e8c77fb300 100644 --- a/libs/server/Auth/GarnetACLAuthenticator.cs +++ b/libs/server/Auth/GarnetACLAuthenticator.cs @@ -69,15 +69,15 @@ public bool Authenticate(ReadOnlySpan password, ReadOnlySpan usernam // Check if user exists and set default user if username is unspecified string uname = Encoding.ASCII.GetString(username); User user = string.IsNullOrEmpty(uname) ? _acl.GetDefaultUser() : _acl.GetUser(uname); - - if(user == null) - { - return false; - } // Use injected authenticator if configured. if (_authenticator != null) { + if (user == null || password.Length == 0) + { + return false; + } + if (user.IsEnabled && _authenticator.Authenticate(password, username)) { _user = user; diff --git a/libs/server/Auth/GarnetAadAuthenticator.cs b/libs/server/Auth/GarnetAadAuthenticator.cs index 211637d7ba..a7c21a8057 100644 --- a/libs/server/Auth/GarnetAadAuthenticator.cs +++ b/libs/server/Auth/GarnetAadAuthenticator.cs @@ -19,11 +19,8 @@ internal class AadValidationConfig internal IReadOnlyCollection _authorizedAppIds { get; set; } internal IReadOnlyCollection _audiences { get; set; } internal IReadOnlyCollection _issuers { get; set; } - internal IssuerSigningTokenProvider _signingTokenProvider { get; set; } - - internal bool ValidateUsername { get; set; } - + internal bool ValidateUsername { get; set; } } class GarnetAadAuthenticator : IGarnetAuthenticator @@ -89,12 +86,7 @@ public bool Authenticate(ReadOnlySpan password, ReadOnlySpan usernam IssuerSigningKeys = _signingTokenProvider.SigningTokens }; parameters.EnableAadSigningKeyIssuerValidation(); - var passwordStr = Encoding.UTF8.GetString(password); - if (string.IsNullOrWhiteSpace(passwordStr)) - { - return false; - } - var identity = _tokenHandler.ValidateToken(passwordStr, parameters, out var token); + var identity = _tokenHandler.ValidateToken(Encoding.UTF8.GetString(password), parameters, out var token); _validFrom = token.ValidFrom; _validateTo = token.ValidTo; @@ -120,10 +112,9 @@ private bool IsIdentityAuthorized(ClaimsPrincipal identity, ReadOnlySpan u .GroupBy(claim => claim.Type) .ToDictionary(group => group.Key, group => string.Join(',', group.Select(c => c.Value)), StringComparer.OrdinalIgnoreCase); - bool isValid = IsApplicationPrincipal(claims) && IsApplicationAuthorized(claims); + bool isValid = IsApplicationPrincipal(claims) && IsApplicationAuthorized(claims); return !ValidateUsername ? isValid : ValidateUsername && IsUserNameAuthorized(claims, userName); } - private bool IsApplicationAuthorized(IDictionary claims) { return claims.TryGetValue(_appIdClaim, out var appId) && _authorizedAppIds.Contains(appId); diff --git a/test/Garnet.test.cluster/ClusterAadAuthTests.cs b/test/Garnet.test.cluster/ClusterAadAuthTests.cs index 97a411bc2e..e0c9b8cf4e 100644 --- a/test/Garnet.test.cluster/ClusterAadAuthTests.cs +++ b/test/Garnet.test.cluster/ClusterAadAuthTests.cs @@ -56,7 +56,7 @@ public void ValidateClusterAuth() var token = tokenGenerator.CreateToken(tokenClaims, DateTime.Now.AddMinutes(10)); // Generate default ACL file - var userCredential = new ServerCredential { user = objId, IsAdmin= true, IsClearText = true }; + var userCredential = new ServerCredential { user = objId, IsAdmin = true, IsClearText = true }; var clientCredentials = new ServerCredential { user = objId, password = token }; context.GenerateCredentials([userCredential]); context.CreateInstances(nodes, useAcl: true, clusterCreds: clientCredentials, authenticationSettings: authSettings); @@ -71,7 +71,6 @@ public void ValidateClusterAuth() var ex = Assert.Throws(() => context.clusterTestUtils.Authenticate(i, "randomUserId", clientCredentials.password, context.logger)); Assert.AreEqual("WRONGPASS Invalid username/password combination", ex.Message); } - } } -} +} \ No newline at end of file diff --git a/test/Garnet.test.cluster/JwtTokenGenerator.cs b/test/Garnet.test.cluster/JwtTokenGenerator.cs index 59e90e05a2..d41a30d3e4 100644 --- a/test/Garnet.test.cluster/JwtTokenGenerator.cs +++ b/test/Garnet.test.cluster/JwtTokenGenerator.cs @@ -25,11 +25,11 @@ internal class JwtTokenGenerator // the token handler we'll use to actually issue tokens public readonly JwtSecurityTokenHandler JwtSecurityTokenHandler = new(); - internal JwtTokenGenerator(string issuer) + internal JwtTokenGenerator(string issuer) { Issuer = issuer; - SecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("this is my custom Secret key for authentication")); - SigningCredentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256); + SecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Test secret key for authentication and signing the token to be generated")); + SigningCredentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256); } internal string CreateToken(List claims, DateTime expiryTime) @@ -38,4 +38,4 @@ internal string CreateToken(List claims, DateTime expiryTime) } } -} +} \ No newline at end of file From 63713d30c67cb6328676da34d2cc2ccb5da87757 Mon Sep 17 00:00:00 2001 From: Padmanabh Gupta Date: Wed, 15 May 2024 13:56:00 +0530 Subject: [PATCH 03/11] - Added AADValidateUsername flag - Refactor to AclWithAad and AclWithPassword hierarchies - fix naming convention for private member _validateUsername - add comments and fix tests --- libs/host/Configuration/Options.cs | 11 +- libs/server/Auth/AuthenticationSettings.cs | 100 +++++++++++++----- libs/server/Auth/GarnetACLAuthenticator.cs | 41 ++----- libs/server/Auth/GarnetAadAuthenticator.cs | 26 +---- .../Auth/GarnetAclWithAadAuthenticator.cs | 40 +++++++ .../GarnetAclWithPasswordAuthenticator.cs | 40 +++++++ libs/server/StoreWrapper.cs | 2 +- .../ClusterAadAuthTests.cs | 1 - test/Garnet.test/TestUtils.cs | 11 +- 9 files changed, 183 insertions(+), 89 deletions(-) create mode 100644 libs/server/Auth/GarnetAclWithAadAuthenticator.cs create mode 100644 libs/server/Auth/GarnetAclWithPasswordAuthenticator.cs diff --git a/libs/host/Configuration/Options.cs b/libs/host/Configuration/Options.cs index 36ccaf5cb7..3ea9d601da 100644 --- a/libs/host/Configuration/Options.cs +++ b/libs/host/Configuration/Options.cs @@ -167,6 +167,9 @@ internal sealed class Options [Option("aad-authorized-app-ids", Required = false, Separator = ',', HelpText = "The authorized client app Ids for AAD authentication. Should be a comma separated string.")] public string AuthorizedAadApplicationIds { get; set; } + [Option("aad-validate-acl-username", Required = false, Separator = ',', HelpText = "Only valid for AclWithAAD mode. Validates username - expected to be OID ag")] + public bool AadValidateUsername { get; set; } + [OptionValidation] [Option("aof", Required = false, HelpText = "Enable write ahead logging (append-only file).")] public bool? EnableAOF { get; set; } @@ -623,12 +626,10 @@ private IAuthenticationSettings GetAuthenticationSettings(ILogger logger = null) case GarnetAuthenticationMode.Aad: return new AadAuthenticationSettings(AuthorizedAadApplicationIds?.Split(','), AadAudiences?.Split(','), AadIssuers?.Split(','), IssuerSigningTokenProvider.Create(AadAuthority, logger)); case GarnetAuthenticationMode.ACL: - return new AclAuthenticationSettings(AclFile, Password); + return new AclAuthenticationPasswordSettings(AclFile, Password); case GarnetAuthenticationMode.AclWithAad: - var aadAuthSettings = new AadAuthenticationSettings(AuthorizedAadApplicationIds?.Split(','), AadAudiences?.Split(','), AadIssuers?.Split(','), IssuerSigningTokenProvider.Create(AadAuthority, logger)); - aadAuthSettings = aadAuthSettings.WithUsernameValidation(); - return new AclAuthenticationSettings(AclFile, Password, aadAuthSettings); - + var aadAuthSettings = new AadAuthenticationSettings(AuthorizedAadApplicationIds?.Split(','), AadAudiences?.Split(','), AadIssuers?.Split(','), IssuerSigningTokenProvider.Create(AadAuthority, logger), AadValidateUsername); + return new AclAuthenticationAadSettings(AclFile, Password, aadAuthSettings); default: logger?.LogError("Unsupported authentication mode: {mode}", AuthenticationMode); throw new Exception($"Authentication mode {AuthenticationMode} is not supported."); diff --git a/libs/server/Auth/AuthenticationSettings.cs b/libs/server/Auth/AuthenticationSettings.cs index 5f2c1b6844..726a0c6c23 100644 --- a/libs/server/Auth/AuthenticationSettings.cs +++ b/libs/server/Auth/AuthenticationSettings.cs @@ -129,8 +129,9 @@ public class AadAuthenticationSettings : IAuthenticationSettings /// Allowed app Ids /// Allowed audiences /// Allowed issuers - /// Signing token provider - public AadAuthenticationSettings(string[] authorizedAppIds, string[] audiences, string[] issuers, IssuerSigningTokenProvider signingTokenProvier) + /// Signing token provider + /// whether to validate username or not. + public AadAuthenticationSettings(string[] authorizedAppIds, string[] audiences, string[] issuers, IssuerSigningTokenProvider signingTokenProvider, bool validateUsername = false) { if (authorizedAppIds == null || authorizedAppIds.Length == 0) { @@ -147,7 +148,7 @@ public AadAuthenticationSettings(string[] authorizedAppIds, string[] audiences, throw new Exception("Issuers cannot be empty."); } - if (signingTokenProvier == null) + if (signingTokenProvider == null) { throw new Exception("Signing token provider cannot be null."); } @@ -155,13 +156,8 @@ public AadAuthenticationSettings(string[] authorizedAppIds, string[] audiences, _authorizedAppIds = new HashSet(authorizedAppIds, StringComparer.OrdinalIgnoreCase); _audiences = new HashSet(audiences, StringComparer.OrdinalIgnoreCase); _issuers = new HashSet(issuers, StringComparer.OrdinalIgnoreCase); - _signingTokenProvider = signingTokenProvier; - } - - public AadAuthenticationSettings WithUsernameValidation() - { - _validateUsername = true; - return this; + _signingTokenProvider = signingTokenProvider; + _validateUsername = validateUsername; } /// @@ -170,15 +166,7 @@ public AadAuthenticationSettings WithUsernameValidation() /// The main store the authenticator will be associated with. public IGarnetAuthenticator CreateAuthenticator(StoreWrapper storeWrapper) { - var config = new AadValidationConfig - { - _audiences = _audiences, - _authorizedAppIds = _authorizedAppIds, - _issuers = _issuers, - _signingTokenProvider = _signingTokenProvider, - ValidateUsername = _validateUsername, - }; - return new GarnetAadAuthenticator(config, storeWrapper.logger); + return new GarnetAadAuthenticator(_authorizedAppIds, _audiences, _issuers, _signingTokenProvider, _validateUsername, storeWrapper.logger); } /// @@ -212,7 +200,7 @@ public void Dispose() /// /// ACL authentication settings /// - public class AclAuthenticationSettings : IAuthenticationSettings + public abstract class AclAuthenticationSettings : IAuthenticationSettings { /// /// Location of a the ACL configuration file to load users from @@ -224,18 +212,15 @@ public class AclAuthenticationSettings : IAuthenticationSettings /// public readonly string DefaultPassword; - private IAuthenticationSettings settings; - /// /// Creates and initializes new ACL authentication settings /// /// Location of the ACL configuration file /// Optional default password, if not defined through aclConfigurationFile - public AclAuthenticationSettings(string aclConfigurationFile, string defaultPassword = "", IAuthenticationSettings settings = null) + public AclAuthenticationSettings(string aclConfigurationFile, string defaultPassword = "") { AclConfigurationFile = aclConfigurationFile; DefaultPassword = defaultPassword; - this.settings = settings; } /// @@ -244,9 +229,17 @@ public AclAuthenticationSettings(string aclConfigurationFile, string defaultPass /// The main store the authenticator will be associated with. public IGarnetAuthenticator CreateAuthenticator(StoreWrapper storeWrapper) { - return new GarnetACLAuthenticator(storeWrapper.accessControlList, storeWrapper.logger, settings?.CreateAuthenticator(storeWrapper)); + return CreateAuthenticatorInternal(storeWrapper); } + /// + /// Creates the internal implementation specific ACL authenticator. + /// + /// The main store the authenticator will be associated with. + /// IGarnetAuthenticator instance + protected abstract IGarnetAuthenticator CreateAuthenticatorInternal(StoreWrapper storeWrapper); + + /// /// Dispose /// @@ -255,4 +248,61 @@ public void Dispose() // No-op } } + + /// + /// ACL authentication with AAD settings. + /// + public class AclAuthenticationPasswordSettings : AclAuthenticationSettings + { + + /// + /// Creates and initializes new ACL authentication settings + /// + /// Location of the ACL configuration file + /// Optional default password, if not defined through aclConfigurationFile + public AclAuthenticationPasswordSettings(string aclConfigurationFile, string defaultPassword = "") : base(aclConfigurationFile, defaultPassword) + { + } + + /// + /// Creates an ACL authenticator + /// + /// The main store the authenticator will be associated with. + + protected override IGarnetAuthenticator CreateAuthenticatorInternal(StoreWrapper storeWrapper) + { + return new GarnetAclWithPasswordAuthenticator(storeWrapper.accessControlList, storeWrapper.logger); + } + } + + + /// + /// ACL authentication with AAD settings + /// + public class AclAuthenticationAadSettings : AclAuthenticationSettings + { + + AadAuthenticationSettings _aadAuthenticationSettings; + + /// + /// Creates and initializes new ACL authentication settings + /// + /// Location of the ACL configuration file + /// Optional default password, if not defined through aclConfigurationFile + /// AAD settings used for authentication + public AclAuthenticationAadSettings(string aclConfigurationFile, string defaultPassword = "", AadAuthenticationSettings aadAuthenticationSettings = null) : base(aclConfigurationFile, defaultPassword) + { + _aadAuthenticationSettings = aadAuthenticationSettings; + } + + /// + /// Creates an ACL authenticator + /// + /// The main store the authenticator will be associated with. + + protected override IGarnetAuthenticator CreateAuthenticatorInternal(StoreWrapper storeWrapper) + { + return new GarnetAclWithAadAuthenticator(storeWrapper.accessControlList, _aadAuthenticationSettings.CreateAuthenticator(storeWrapper), storeWrapper.logger); + } + } } \ No newline at end of file diff --git a/libs/server/Auth/GarnetACLAuthenticator.cs b/libs/server/Auth/GarnetACLAuthenticator.cs index e8c77fb300..32636d6184 100644 --- a/libs/server/Auth/GarnetACLAuthenticator.cs +++ b/libs/server/Auth/GarnetACLAuthenticator.cs @@ -8,36 +8,32 @@ namespace Garnet.server.Auth { - class GarnetACLAuthenticator : IGarnetAuthenticator + abstract class GarnetACLAuthenticator : IGarnetAuthenticator { /// /// The Access Control List to authenticate users against /// - readonly AccessControlList _acl; + protected readonly AccessControlList _acl; /// /// Logger to use to output log messages to /// - readonly ILogger _logger; + protected readonly ILogger _logger; /// /// If authenticated, contains a reference to the authenticated user. Otherwise null. /// - User _user = null; - - - private IGarnetAuthenticator _authenticator; + protected User _user = null; /// /// Initializes a new ACLAuthenticator instance. /// /// Access control list to authenticate against /// The logger to use - public GarnetACLAuthenticator(AccessControlList accessControlList, ILogger logger, IGarnetAuthenticator wrapperAuthenticator = null) + public GarnetACLAuthenticator(AccessControlList accessControlList, ILogger logger) { _acl = accessControlList; _logger = logger; - _authenticator = wrapperAuthenticator; } /// @@ -69,30 +65,11 @@ public bool Authenticate(ReadOnlySpan password, ReadOnlySpan usernam // Check if user exists and set default user if username is unspecified string uname = Encoding.ASCII.GetString(username); User user = string.IsNullOrEmpty(uname) ? _acl.GetDefaultUser() : _acl.GetUser(uname); - - // Use injected authenticator if configured. - if (_authenticator != null) - { - if (user == null || password.Length == 0) - { - return false; - } - - if (user.IsEnabled && _authenticator.Authenticate(password, username)) - { - _user = user; - successful = true; - } - return successful; - } - - // Try to authenticate user - ACLPassword passwordHash = ACLPassword.ACLPasswordFromString(Encoding.ASCII.GetString(password)); - if (user.IsEnabled && user.ValidatePassword(passwordHash)) + if (user == null) { - _user = user; - successful = true; + return false; } + successful = AuthenticateInternal(user, username, password); } catch (Exception ex) { @@ -103,6 +80,8 @@ public bool Authenticate(ReadOnlySpan password, ReadOnlySpan usernam return successful; } + protected abstract bool AuthenticateInternal(User user, ReadOnlySpan username, ReadOnlySpan password); + /// /// Returns the currently authorized user. /// diff --git a/libs/server/Auth/GarnetAadAuthenticator.cs b/libs/server/Auth/GarnetAadAuthenticator.cs index a7c21a8057..e46e9553b8 100644 --- a/libs/server/Auth/GarnetAadAuthenticator.cs +++ b/libs/server/Auth/GarnetAadAuthenticator.cs @@ -14,15 +14,6 @@ namespace Garnet.server.Auth { - internal class AadValidationConfig - { - internal IReadOnlyCollection _authorizedAppIds { get; set; } - internal IReadOnlyCollection _audiences { get; set; } - internal IReadOnlyCollection _issuers { get; set; } - internal IssuerSigningTokenProvider _signingTokenProvider { get; set; } - internal bool ValidateUsername { get; set; } - } - class GarnetAadAuthenticator : IGarnetAuthenticator { private static JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler(); @@ -45,7 +36,7 @@ class GarnetAadAuthenticator : IGarnetAuthenticator private readonly IReadOnlyCollection _audiences; private readonly IReadOnlyCollection _issuers; private readonly IssuerSigningTokenProvider _signingTokenProvider; - private readonly bool ValidateUsername; + private readonly bool _validateUsername; private readonly ILogger _logger; @@ -54,23 +45,14 @@ public GarnetAadAuthenticator( IReadOnlyCollection audiences, IReadOnlyCollection issuers, IssuerSigningTokenProvider signingTokenProvider, + bool validateUsername, ILogger logger) { _authorizedAppIds = authorizedAppIds; _signingTokenProvider = signingTokenProvider; _audiences = audiences; _issuers = issuers; - ValidateUsername = false; - _logger = logger; - } - - internal GarnetAadAuthenticator(AadValidationConfig aadValidationConfig, ILogger logger) - { - _authorizedAppIds = aadValidationConfig._authorizedAppIds; - _signingTokenProvider = aadValidationConfig._signingTokenProvider; - _audiences = aadValidationConfig._audiences; - _issuers = aadValidationConfig._issuers; - ValidateUsername = aadValidationConfig.ValidateUsername; + _validateUsername = validateUsername; _logger = logger; } @@ -113,7 +95,7 @@ private bool IsIdentityAuthorized(ClaimsPrincipal identity, ReadOnlySpan u .ToDictionary(group => group.Key, group => string.Join(',', group.Select(c => c.Value)), StringComparer.OrdinalIgnoreCase); bool isValid = IsApplicationPrincipal(claims) && IsApplicationAuthorized(claims); - return !ValidateUsername ? isValid : ValidateUsername && IsUserNameAuthorized(claims, userName); + return !_validateUsername ? isValid : _validateUsername && IsUserNameAuthorized(claims, userName); } private bool IsApplicationAuthorized(IDictionary claims) { diff --git a/libs/server/Auth/GarnetAclWithAadAuthenticator.cs b/libs/server/Auth/GarnetAclWithAadAuthenticator.cs new file mode 100644 index 0000000000..1c121c940f --- /dev/null +++ b/libs/server/Auth/GarnetAclWithAadAuthenticator.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Garnet.server.ACL; +using Microsoft.Extensions.Logging; + +namespace Garnet.server.Auth +{ + class GarnetAclWithAadAuthenticator : GarnetACLAuthenticator + { + + private readonly IGarnetAuthenticator _garnetAuthenticator; + public GarnetAclWithAadAuthenticator(AccessControlList accessControlList, IGarnetAuthenticator garnetAuthenticator, ILogger logger) : base(accessControlList, logger) + { + _garnetAuthenticator = garnetAuthenticator; + } + + /// + /// Authenticate the given user/password combination. + /// + /// User details to use for authentication. + /// Password to authenticate with. + /// Username to authenticate with. If empty, will authenticate default user. + /// true if authentication was successful + protected override bool AuthenticateInternal(User user, ReadOnlySpan username, ReadOnlySpan password) + { + if (user.IsEnabled && _garnetAuthenticator.Authenticate(password, username)) + { + _user = user; + return true; + } + return false; + } + } +} diff --git a/libs/server/Auth/GarnetAclWithPasswordAuthenticator.cs b/libs/server/Auth/GarnetAclWithPasswordAuthenticator.cs new file mode 100644 index 0000000000..6912c9bb49 --- /dev/null +++ b/libs/server/Auth/GarnetAclWithPasswordAuthenticator.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Garnet.server.ACL; +using Microsoft.Extensions.Logging; + +namespace Garnet.server.Auth +{ + class GarnetAclWithPasswordAuthenticator : GarnetACLAuthenticator + { + + public GarnetAclWithPasswordAuthenticator(AccessControlList accessControlList, ILogger logger) : base(accessControlList, logger) + { + } + + /// + /// Authenticate the given user/password combination. + /// + /// User details to use for authentication. + /// Password to authenticate with. + /// Username to authenticate with. If empty, will authenticate default user. + /// true if authentication was successful + protected override bool AuthenticateInternal(User user, ReadOnlySpan username, ReadOnlySpan password) + { + // Try to authenticate user + ACLPassword passwordHash = ACLPassword.ACLPasswordFromString(Encoding.ASCII.GetString(password)); + if (user.IsEnabled && user.ValidatePassword(passwordHash)) + { + _user = user; + return true; + } + return false; + } + } +} diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 6bf134f360..9007cc1703 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -135,7 +135,7 @@ public StoreWrapper( // If ACL authentication is enabled, initiate access control list // NOTE: This is a temporary workflow. ACL should always be initiated and authenticator // should become a parameter of AccessControlList. - if ((this.serverOptions.AuthSettings != null) && (this.serverOptions.AuthSettings.GetType() == typeof(AclAuthenticationSettings))) + if ((this.serverOptions.AuthSettings != null) && (this.serverOptions.AuthSettings.GetType().BaseType == typeof(AclAuthenticationSettings))) { // Create a new access control list and register it with the authentication settings AclAuthenticationSettings aclAuthenticationSettings = (AclAuthenticationSettings)this.serverOptions.AuthSettings; diff --git a/test/Garnet.test.cluster/ClusterAadAuthTests.cs b/test/Garnet.test.cluster/ClusterAadAuthTests.cs index e0c9b8cf4e..e353764d3b 100644 --- a/test/Garnet.test.cluster/ClusterAadAuthTests.cs +++ b/test/Garnet.test.cluster/ClusterAadAuthTests.cs @@ -52,7 +52,6 @@ public void ValidateClusterAuth() new Claim("http://schemas.microsoft.com/identity/claims/objectidentifier",objId), }; var authSettings = new AadAuthenticationSettings([appId], [audience], [audience], IssuerSigningTokenProvider.Create(new List { tokenGenerator.SecurityKey }, context.logger)); - authSettings = authSettings.WithUsernameValidation(); var token = tokenGenerator.CreateToken(tokenClaims, DateTime.Now.AddMinutes(10)); // Generate default ACL file diff --git a/test/Garnet.test/TestUtils.cs b/test/Garnet.test/TestUtils.cs index 33e8274977..48fed8eef4 100644 --- a/test/Garnet.test/TestUtils.cs +++ b/test/Garnet.test/TestUtils.cs @@ -190,7 +190,7 @@ public static GarnetServer CreateGarnetServer( IAuthenticationSettings authenticationSettings = null; if (useAcl) { - authenticationSettings = new AclAuthenticationSettings(aclFile, defaultPassword); + authenticationSettings = new AclAuthenticationPasswordSettings(aclFile, defaultPassword); } else if (defaultPassword != null) { @@ -415,10 +415,13 @@ public static GarnetServerOptions GetGarnetServerOptions( if (!UseAzureStorage) _CheckpointDir = new DirectoryInfo(string.IsNullOrEmpty(_CheckpointDir) ? "." : _CheckpointDir).FullName; IAuthenticationSettings authenticationSettings = null; - if (useAcl) + if (useAcl && aadAuthenticationSettings != null) { - - authenticationSettings = new AclAuthenticationSettings(aclFile, authPassword, aadAuthenticationSettings); + authenticationSettings = new AclAuthenticationAadSettings(aclFile, authPassword, aadAuthenticationSettings); + } + else if (useAcl) + { + authenticationSettings = new AclAuthenticationPasswordSettings(aclFile, authPassword); } else if (authPassword != null) { From 1375716c1c1af8f5ba98ba00c637bd1cd309a5d2 Mon Sep 17 00:00:00 2001 From: Padmanabh Gupta Date: Wed, 15 May 2024 14:03:42 +0530 Subject: [PATCH 04/11] fomratting fixes --- libs/server/Auth/GarnetAclWithAadAuthenticator.cs | 6 ++++-- libs/server/Auth/GarnetAclWithPasswordAuthenticator.cs | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/libs/server/Auth/GarnetAclWithAadAuthenticator.cs b/libs/server/Auth/GarnetAclWithAadAuthenticator.cs index 1c121c940f..9c3f93e695 100644 --- a/libs/server/Auth/GarnetAclWithAadAuthenticator.cs +++ b/libs/server/Auth/GarnetAclWithAadAuthenticator.cs @@ -13,7 +13,9 @@ namespace Garnet.server.Auth { class GarnetAclWithAadAuthenticator : GarnetACLAuthenticator { - + /// + /// Authenticator to validate username and password. + /// private readonly IGarnetAuthenticator _garnetAuthenticator; public GarnetAclWithAadAuthenticator(AccessControlList accessControlList, IGarnetAuthenticator garnetAuthenticator, ILogger logger) : base(accessControlList, logger) { @@ -37,4 +39,4 @@ protected override bool AuthenticateInternal(User user, ReadOnlySpan usern return false; } } -} +} \ No newline at end of file diff --git a/libs/server/Auth/GarnetAclWithPasswordAuthenticator.cs b/libs/server/Auth/GarnetAclWithPasswordAuthenticator.cs index 6912c9bb49..d29fc61497 100644 --- a/libs/server/Auth/GarnetAclWithPasswordAuthenticator.cs +++ b/libs/server/Auth/GarnetAclWithPasswordAuthenticator.cs @@ -27,7 +27,7 @@ public GarnetAclWithPasswordAuthenticator(AccessControlList accessControlList, I /// true if authentication was successful protected override bool AuthenticateInternal(User user, ReadOnlySpan username, ReadOnlySpan password) { - // Try to authenticate user + // Try to authenticate user ACLPassword passwordHash = ACLPassword.ACLPasswordFromString(Encoding.ASCII.GetString(password)); if (user.IsEnabled && user.ValidatePassword(passwordHash)) { @@ -37,4 +37,4 @@ protected override bool AuthenticateInternal(User user, ReadOnlySpan usern return false; } } -} +} \ No newline at end of file From 14602bc59cf5cbd76c431e4129f821d2119c1f4c Mon Sep 17 00:00:00 2001 From: Padmanabh Gupta Date: Wed, 15 May 2024 14:08:37 +0530 Subject: [PATCH 05/11] fix logger parameter positioning --- libs/server/Auth/Aad/IssuerSigningTokenProvider.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/server/Auth/Aad/IssuerSigningTokenProvider.cs b/libs/server/Auth/Aad/IssuerSigningTokenProvider.cs index 2553b543e6..8dbdba3869 100644 --- a/libs/server/Auth/Aad/IssuerSigningTokenProvider.cs +++ b/libs/server/Auth/Aad/IssuerSigningTokenProvider.cs @@ -31,7 +31,7 @@ public class IssuerSigningTokenProvider : IDisposable private readonly ILogger _logger; - private IssuerSigningTokenProvider(string authority, IReadOnlyCollection signingTokens, ILogger logger, bool refreshTokens = true) + private IssuerSigningTokenProvider(string authority, IReadOnlyCollection signingTokens, bool refreshTokens, ILogger logger) { _authority = authority; if (refreshTokens) @@ -107,7 +107,7 @@ public static IssuerSigningTokenProvider Create(string authority, ILogger logger } var signingTokens = RetrieveSigningTokens(authority); - return new IssuerSigningTokenProvider(authority, signingTokens, logger); + return new IssuerSigningTokenProvider(authority, signingTokens, refreshTokens: true, logger); } /// @@ -118,7 +118,7 @@ public static IssuerSigningTokenProvider Create(string authority, ILogger logger /// The logger public static IssuerSigningTokenProvider Create(IReadOnlyCollection signingToken, ILogger logger) { - return new IssuerSigningTokenProvider(string.Empty, signingToken, logger, refreshTokens: false); + return new IssuerSigningTokenProvider(string.Empty, signingToken, refreshTokens: false, logger); } } } \ No newline at end of file From 24d129b01499a2b265a302498390d4d8a3c19105 Mon Sep 17 00:00:00 2001 From: Padmanabh Gupta Date: Wed, 15 May 2024 14:10:47 +0530 Subject: [PATCH 06/11] fix parameter ordering for logger --- test/Garnet.test/TestUtils.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Garnet.test/TestUtils.cs b/test/Garnet.test/TestUtils.cs index 48fed8eef4..786285f7cb 100644 --- a/test/Garnet.test/TestUtils.cs +++ b/test/Garnet.test/TestUtils.cs @@ -399,8 +399,8 @@ public static GarnetServerOptions GetGarnetServerOptions( bool useAcl = false, // NOTE: Temporary until ACL is enforced as default string aclFile = null, X509CertificateCollection certificates = null, - ILogger logger = null, - AadAuthenticationSettings aadAuthenticationSettings = null) + AadAuthenticationSettings aadAuthenticationSettings = null, + ILogger logger = null) { if (UseAzureStorage) IgnoreIfNotRunningAzureTests(); From d074d237f2d2d70ef07831ee79ebbf73a335bf91 Mon Sep 17 00:00:00 2001 From: Padmanabh Gupta Date: Wed, 15 May 2024 20:54:46 +0530 Subject: [PATCH 07/11] add default value --- libs/host/Configuration/Options.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/host/Configuration/Options.cs b/libs/host/Configuration/Options.cs index 3ea9d601da..4a30200916 100644 --- a/libs/host/Configuration/Options.cs +++ b/libs/host/Configuration/Options.cs @@ -168,7 +168,7 @@ internal sealed class Options public string AuthorizedAadApplicationIds { get; set; } [Option("aad-validate-acl-username", Required = false, Separator = ',', HelpText = "Only valid for AclWithAAD mode. Validates username - expected to be OID ag")] - public bool AadValidateUsername { get; set; } + public bool? AadValidateUsername { get; set; } [OptionValidation] [Option("aof", Required = false, HelpText = "Enable write ahead logging (append-only file).")] @@ -628,7 +628,7 @@ private IAuthenticationSettings GetAuthenticationSettings(ILogger logger = null) case GarnetAuthenticationMode.ACL: return new AclAuthenticationPasswordSettings(AclFile, Password); case GarnetAuthenticationMode.AclWithAad: - var aadAuthSettings = new AadAuthenticationSettings(AuthorizedAadApplicationIds?.Split(','), AadAudiences?.Split(','), AadIssuers?.Split(','), IssuerSigningTokenProvider.Create(AadAuthority, logger), AadValidateUsername); + var aadAuthSettings = new AadAuthenticationSettings(AuthorizedAadApplicationIds?.Split(','), AadAudiences?.Split(','), AadIssuers?.Split(','), IssuerSigningTokenProvider.Create(AadAuthority, logger), AadValidateUsername.GetValueOrDefault()); return new AclAuthenticationAadSettings(AclFile, Password, aadAuthSettings); default: logger?.LogError("Unsupported authentication mode: {mode}", AuthenticationMode); From 91ad7b604b1ad73a9e1b59a5fc5171df5af16ccb Mon Sep 17 00:00:00 2001 From: Padmanabh Gupta Date: Wed, 15 May 2024 23:06:02 +0530 Subject: [PATCH 08/11] add base type assertions --- libs/server/Resp/ACLCommands.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/server/Resp/ACLCommands.cs b/libs/server/Resp/ACLCommands.cs index 474d90068e..64a4522530 100644 --- a/libs/server/Resp/ACLCommands.cs +++ b/libs/server/Resp/ACLCommands.cs @@ -27,7 +27,7 @@ private bool ProcessACLCommands(ReadOnlySpan bufSpan, int count) { // Only proceed if current authenticator can be used with ACL commands. // Currently only GarnetACLAuthenticator is supported. - if (!_authenticator.HasACLSupport || (_authenticator.GetType() != typeof(GarnetACLAuthenticator))) + if (!_authenticator.HasACLSupport || (_authenticator.GetType().BaseType != typeof(GarnetACLAuthenticator))) { if (!DrainCommands(bufSpan, count)) return false; @@ -214,7 +214,7 @@ private bool ProcessACLCommands(ReadOnlySpan bufSpan, int count) // NOTE: This is temporary as long as ACL operations are only supported when using the ACL authenticator Debug.Assert(this.storeWrapper.serverOptions.AuthSettings != null); - Debug.Assert(this.storeWrapper.serverOptions.AuthSettings.GetType() == typeof(AclAuthenticationSettings)); + Debug.Assert(this.storeWrapper.serverOptions.AuthSettings.GetType().BaseType == typeof(AclAuthenticationSettings)); AclAuthenticationSettings aclAuthenticationSettings = (AclAuthenticationSettings)this.storeWrapper.serverOptions.AuthSettings; // Try to reload the configured ACL configuration file From 42a237ab59ee6568df5a3a3874a798e0cc422c58 Mon Sep 17 00:00:00 2001 From: Padmanabh Gupta Date: Thu, 16 May 2024 10:39:43 +0530 Subject: [PATCH 09/11] add default value - failing test due to this. --- libs/host/defaults.conf | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libs/host/defaults.conf b/libs/host/defaults.conf index 0005aaaab3..38dd3643ac 100644 --- a/libs/host/defaults.conf +++ b/libs/host/defaults.conf @@ -111,6 +111,9 @@ /* The authorized client app Ids for AAD authentication. Should be a comma separated string. */ "AuthorizedAadApplicationIds" : null, + /* Whether to validate username as ObjectId - meant to be used with ACL setup. */ + "AadValidateUsername": false, + /* Enable write ahead logging (append-only file). */ "EnableAOF" : false, From 79972904a0bd9dcea3ad462987a6c90162ffaf69 Mon Sep 17 00:00:00 2001 From: Padmanabh Gupta Date: Fri, 17 May 2024 17:44:34 +0530 Subject: [PATCH 10/11] - Fix comments to account for groupId - Add support for groupId check and corresponding test --- libs/host/Configuration/Options.cs | 2 +- libs/host/defaults.conf | 2 +- libs/server/Auth/AuthenticationSettings.cs | 2 +- libs/server/Auth/GarnetAadAuthenticator.cs | 24 +++++++++- .../Auth/GarnetAclWithAadAuthenticator.cs | 2 +- .../ClusterAadAuthTests.cs | 44 +++++++++++++++---- 6 files changed, 63 insertions(+), 13 deletions(-) diff --git a/libs/host/Configuration/Options.cs b/libs/host/Configuration/Options.cs index 4a30200916..90f163986f 100644 --- a/libs/host/Configuration/Options.cs +++ b/libs/host/Configuration/Options.cs @@ -167,7 +167,7 @@ internal sealed class Options [Option("aad-authorized-app-ids", Required = false, Separator = ',', HelpText = "The authorized client app Ids for AAD authentication. Should be a comma separated string.")] public string AuthorizedAadApplicationIds { get; set; } - [Option("aad-validate-acl-username", Required = false, Separator = ',', HelpText = "Only valid for AclWithAAD mode. Validates username - expected to be OID ag")] + [Option("aad-validate-acl-username", Required = false, Separator = ',', HelpText = "Only valid for AclWithAAD mode. Validates username - expected to be OID of client app or a valid group's object id of which the client is part of.")] public bool? AadValidateUsername { get; set; } [OptionValidation] diff --git a/libs/host/defaults.conf b/libs/host/defaults.conf index 38dd3643ac..ac8288c5d4 100644 --- a/libs/host/defaults.conf +++ b/libs/host/defaults.conf @@ -111,7 +111,7 @@ /* The authorized client app Ids for AAD authentication. Should be a comma separated string. */ "AuthorizedAadApplicationIds" : null, - /* Whether to validate username as ObjectId - meant to be used with ACL setup. */ + /* Whether to validate username as ObjectId or a valid Group objectId if present in claims - meant to be used with ACL setup. */ "AadValidateUsername": false, /* Enable write ahead logging (append-only file). */ diff --git a/libs/server/Auth/AuthenticationSettings.cs b/libs/server/Auth/AuthenticationSettings.cs index 726a0c6c23..0b07078dc8 100644 --- a/libs/server/Auth/AuthenticationSettings.cs +++ b/libs/server/Auth/AuthenticationSettings.cs @@ -33,7 +33,7 @@ public enum GarnetAuthenticationMode /// ACL, /// - /// ACL mode using Aad token instead of password. Here username is expected to be ObjectId and token will be validated for claims. + /// ACL mode using Aad token instead of password. Here username is expected to be ObjectId or a valid Group's Object Id and token will be validated for claims. /// AclWithAad } diff --git a/libs/server/Auth/GarnetAadAuthenticator.cs b/libs/server/Auth/GarnetAadAuthenticator.cs index e46e9553b8..0e518bbb9f 100644 --- a/libs/server/Auth/GarnetAadAuthenticator.cs +++ b/libs/server/Auth/GarnetAadAuthenticator.cs @@ -22,6 +22,7 @@ class GarnetAadAuthenticator : IGarnetAuthenticator private const string _scopeClaim = "http://schemas.microsoft.com/identity/claims/scope"; private const string _appIdClaim = "appid"; private const string _oidClaim = "http://schemas.microsoft.com/identity/claims/objectidentifier"; + private const string _groupsClaim = "groups"; public bool IsAuthenticated => IsAuthorized(); @@ -102,10 +103,31 @@ private bool IsApplicationAuthorized(IDictionary claims) return claims.TryGetValue(_appIdClaim, out var appId) && _authorizedAppIds.Contains(appId); } + /// + /// Validates the username for OID or Group claim. A given token issued to client object maybe part of a + /// AAD Group or an ObjectID incase of Application. We validate for OID first and then all groups. + /// + /// token claims mapping + /// input username private bool IsUserNameAuthorized(IDictionary claims, ReadOnlySpan userName) { var userNameStr = Encoding.UTF8.GetString(userName); - return claims.TryGetValue(_oidClaim, out var oid) && userNameStr.Equals(oid, StringComparison.InvariantCultureIgnoreCase); + if (claims.TryGetValue(_oidClaim, out var oid) && oid.Equals(userNameStr, StringComparison.InvariantCultureIgnoreCase)) + { + return true; + } + if (claims.TryGetValue(_groupsClaim, out var groups)) + { + var splitGroups = groups.Split(","); + foreach (var group in splitGroups) + { + if (group.Equals(userNameStr, StringComparison.InvariantCultureIgnoreCase)) + { + return true; + } + } + } + return false; } diff --git a/libs/server/Auth/GarnetAclWithAadAuthenticator.cs b/libs/server/Auth/GarnetAclWithAadAuthenticator.cs index 9c3f93e695..0e26f0d7b8 100644 --- a/libs/server/Auth/GarnetAclWithAadAuthenticator.cs +++ b/libs/server/Auth/GarnetAclWithAadAuthenticator.cs @@ -31,7 +31,7 @@ public GarnetAclWithAadAuthenticator(AccessControlList accessControlList, IGarne /// true if authentication was successful protected override bool AuthenticateInternal(User user, ReadOnlySpan username, ReadOnlySpan password) { - if (user.IsEnabled && _garnetAuthenticator.Authenticate(password, username)) + if (user.IsEnabled && password.Length > 0 && _garnetAuthenticator.Authenticate(password, username)) { _user = user; return true; diff --git a/test/Garnet.test.cluster/ClusterAadAuthTests.cs b/test/Garnet.test.cluster/ClusterAadAuthTests.cs index e353764d3b..0e7b551d01 100644 --- a/test/Garnet.test.cluster/ClusterAadAuthTests.cs +++ b/test/Garnet.test.cluster/ClusterAadAuthTests.cs @@ -37,7 +37,7 @@ public void TearDown() [Test, Order(1)] [Category("CLUSTER-AUTH"), Timeout(60000)] - public void ValidateClusterAuth() + public void ValidateClusterAuthWithObjectId() { var nodes = 2; var audience = Guid.NewGuid().ToString(); @@ -51,25 +51,53 @@ public void ValidateClusterAuth() new Claim("appid", appId), new Claim("http://schemas.microsoft.com/identity/claims/objectidentifier",objId), }; - var authSettings = new AadAuthenticationSettings([appId], [audience], [audience], IssuerSigningTokenProvider.Create(new List { tokenGenerator.SecurityKey }, context.logger)); + var authSettings = new AadAuthenticationSettings([appId], [audience], [audience], IssuerSigningTokenProvider.Create(new List { tokenGenerator.SecurityKey }, context.logger), true); + + var token = tokenGenerator.CreateToken(tokenClaims, DateTime.Now.AddMinutes(10)); + ValidateConnectionsWithToken(objId, token, nodes, authSettings); + } + + [Test, Order(2)] + [Category("CLUSTER-AUTH"), Timeout(60000)] + public void ValidateClusterAuthWithGroupOid() + { + var nodes = 2; + var audience = Guid.NewGuid().ToString(); + JwtTokenGenerator tokenGenerator = new JwtTokenGenerator(audience); + + var appId = Guid.NewGuid().ToString(); + var objId = Guid.NewGuid().ToString(); + var groupIds = new List { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }; + var tokenClaims = new List + { + new Claim("appidacr","1"), + new Claim("appid", appId), + new Claim("http://schemas.microsoft.com/identity/claims/objectidentifier", objId), + new Claim("groups", string.Join(',', groupIds)), + }; + var authSettings = new AadAuthenticationSettings([appId], [audience], [audience], IssuerSigningTokenProvider.Create(new List { tokenGenerator.SecurityKey }, context.logger), true); var token = tokenGenerator.CreateToken(tokenClaims, DateTime.Now.AddMinutes(10)); - // Generate default ACL file + ValidateConnectionsWithToken(groupIds.First(), token, nodes, authSettings); + } - var userCredential = new ServerCredential { user = objId, IsAdmin = true, IsClearText = true }; - var clientCredentials = new ServerCredential { user = objId, password = token }; + private void ValidateConnectionsWithToken(string aclUsername, string token, int nodeCount, AadAuthenticationSettings authenticationSettings) + { + var userCredential = new ServerCredential { user = aclUsername, IsAdmin = true, IsClearText = true }; + var clientCredentials = new ServerCredential { user = aclUsername, password = token }; context.GenerateCredentials([userCredential]); - context.CreateInstances(nodes, useAcl: true, clusterCreds: clientCredentials, authenticationSettings: authSettings); + context.CreateInstances(nodeCount, useAcl: true, clusterCreds: clientCredentials, authenticationSettings: authenticationSettings); context.CreateConnection(useTLS: false, clientCreds: clientCredentials); - for (int i = 0; i < nodes; i++) + for (int i = 0; i < nodeCount; i++) { context.clusterTestUtils.Authenticate(i, clientCredentials.user, clientCredentials.password, context.logger); - context.clusterTestUtils.Meet(i, (i + 1) % nodes, context.logger); + context.clusterTestUtils.Meet(i, (i + 1) % nodeCount, context.logger); var ex = Assert.Throws(() => context.clusterTestUtils.Authenticate(i, "randomUserId", clientCredentials.password, context.logger)); Assert.AreEqual("WRONGPASS Invalid username/password combination", ex.Message); } + } } } \ No newline at end of file From 5496725430d8f84e36545495e468917f35a3e2ac Mon Sep 17 00:00:00 2001 From: Padmanabh Gupta Date: Sat, 18 May 2024 00:26:56 +0530 Subject: [PATCH 11/11] Refactor code to separeate Authentication settings into seprate files and move test constructor for IssuerSigningTokenProvider to test code --- libs/host/Configuration/Options.cs | 2 +- .../Auth/Aad/IssuerSigningTokenProvider.cs | 13 +- libs/server/Auth/AuthenticationSettings.cs | 308 ------------------ .../Settings/AadAuthenticationSettings.cs | 95 ++++++ .../Settings/AclAuthenticationAadSettings.cs | 35 ++ .../AclAuthenticationPasswordSettings.cs | 31 ++ .../Settings/AclAuthenticationSettings.cs | 57 ++++ .../Auth/Settings/AuthenticationSettings.cs | 52 +++ libs/server/Auth/Settings/NoAuthSettings.cs | 28 ++ .../PasswordAuthenticationSettings.cs | 45 +++ libs/server/Resp/ACLCommands.cs | 1 + libs/server/Servers/GarnetServerOptions.cs | 2 +- libs/server/StoreWrapper.cs | 2 +- .../ClusterAadAuthTests.cs | 16 +- .../Garnet.test.cluster/ClusterTestContext.cs | 2 +- ...wtTokenGenerator.cs => JwtTokenHelpers.cs} | 16 +- test/Garnet.test/TestUtils.cs | 2 +- 17 files changed, 372 insertions(+), 335 deletions(-) delete mode 100644 libs/server/Auth/AuthenticationSettings.cs create mode 100644 libs/server/Auth/Settings/AadAuthenticationSettings.cs create mode 100644 libs/server/Auth/Settings/AclAuthenticationAadSettings.cs create mode 100644 libs/server/Auth/Settings/AclAuthenticationPasswordSettings.cs create mode 100644 libs/server/Auth/Settings/AclAuthenticationSettings.cs create mode 100644 libs/server/Auth/Settings/AuthenticationSettings.cs create mode 100644 libs/server/Auth/Settings/NoAuthSettings.cs create mode 100644 libs/server/Auth/Settings/PasswordAuthenticationSettings.cs rename test/Garnet.test.cluster/{JwtTokenGenerator.cs => JwtTokenHelpers.cs} (68%) diff --git a/libs/host/Configuration/Options.cs b/libs/host/Configuration/Options.cs index 90f163986f..6db5ece0f9 100644 --- a/libs/host/Configuration/Options.cs +++ b/libs/host/Configuration/Options.cs @@ -11,8 +11,8 @@ using System.Security.Cryptography.X509Certificates; using CommandLine; using Garnet.server; -using Garnet.server.Auth; using Garnet.server.Auth.Aad; +using Garnet.server.Auth.Settings; using Garnet.server.TLS; using Microsoft.Extensions.Logging; using Tsavorite.core; diff --git a/libs/server/Auth/Aad/IssuerSigningTokenProvider.cs b/libs/server/Auth/Aad/IssuerSigningTokenProvider.cs index 8dbdba3869..261d550e98 100644 --- a/libs/server/Auth/Aad/IssuerSigningTokenProvider.cs +++ b/libs/server/Auth/Aad/IssuerSigningTokenProvider.cs @@ -31,7 +31,7 @@ public class IssuerSigningTokenProvider : IDisposable private readonly ILogger _logger; - private IssuerSigningTokenProvider(string authority, IReadOnlyCollection signingTokens, bool refreshTokens, ILogger logger) + protected IssuerSigningTokenProvider(string authority, IReadOnlyCollection signingTokens, bool refreshTokens = true, ILogger logger = null) { _authority = authority; if (refreshTokens) @@ -109,16 +109,5 @@ public static IssuerSigningTokenProvider Create(string authority, ILogger logger var signingTokens = RetrieveSigningTokens(authority); return new IssuerSigningTokenProvider(authority, signingTokens, refreshTokens: true, logger); } - - /// - /// [Used for testing] - /// Creates an instance of IssuerSigningTokenProvider without refresh of tokens and hence this doesn't need authority. - /// - /// - /// The logger - public static IssuerSigningTokenProvider Create(IReadOnlyCollection signingToken, ILogger logger) - { - return new IssuerSigningTokenProvider(string.Empty, signingToken, refreshTokens: false, logger); - } } } \ No newline at end of file diff --git a/libs/server/Auth/AuthenticationSettings.cs b/libs/server/Auth/AuthenticationSettings.cs deleted file mode 100644 index 0b07078dc8..0000000000 --- a/libs/server/Auth/AuthenticationSettings.cs +++ /dev/null @@ -1,308 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -using System; -using System.Collections.Generic; -using Garnet.server.Auth.Aad; - -namespace Garnet.server.Auth -{ - /// - /// Authentication mode - /// - public enum GarnetAuthenticationMode - { - /// - /// No auth - Garnet accepts any and all connections - /// - NoAuth, - - /// - /// Password - Garnet accepts connections with correct connection string - /// - Password, - - /// - /// AAD - Garnet accepts connection with correct AAD principal - /// In AAD mode, token may expire. Clients are expected to periodically refresh token with Garnet by running AUTH command. - /// - Aad, - - /// - /// ACL - Garnet validates new connections and commands against configured ACL users and access rules. - /// - ACL, - /// - /// ACL mode using Aad token instead of password. Here username is expected to be ObjectId or a valid Group's Object Id and token will be validated for claims. - /// - AclWithAad - } - - /// - /// Authentication settings - /// - public interface IAuthenticationSettings : IDisposable - { - /// - /// Create an authenticator using the current settings. - /// - /// The main store the authenticator will be associated with. - IGarnetAuthenticator CreateAuthenticator(StoreWrapper storeWrapper); - } - - /// - /// No auth settings - /// - public class NoAuthSettings : IAuthenticationSettings - { - /// - /// Creates a no auth authenticator - /// - /// The main store the authenticator will be associated with. - public IGarnetAuthenticator CreateAuthenticator(StoreWrapper storeWrapper) - { - return new GarnetNoAuthAuthenticator(); - } - - /// - /// Dispose - /// - public void Dispose() - { - // No-op - } - } - - /// - /// Password auth settings - /// - public class PasswordAuthenticationSettings : IAuthenticationSettings - { - private readonly byte[] _pwd; - - /// - /// Constructor - /// - /// The password - public PasswordAuthenticationSettings(string pwd) - { - if (string.IsNullOrEmpty(pwd)) - { - throw new Exception("Password cannot be null."); - } - _pwd = System.Text.Encoding.ASCII.GetBytes(pwd); - } - - /// - /// Creates a password auth authenticator - /// - /// The main store the authenticator will be associated with. - public IGarnetAuthenticator CreateAuthenticator(StoreWrapper storeWrapper) - { - return new GarnetPasswordAuthenticator(_pwd); - } - - /// - /// Dispose - /// - public void Dispose() - { - // No op - } - } - - /// - /// AAD auth settings - /// - public class AadAuthenticationSettings : IAuthenticationSettings - { - private readonly IReadOnlyCollection _authorizedAppIds; - private readonly IReadOnlyCollection _audiences; - private readonly IReadOnlyCollection _issuers; - private IssuerSigningTokenProvider _signingTokenProvider; - private bool _validateUsername; - private bool _disposed; - - /// - /// Constructor - /// - /// Allowed app Ids - /// Allowed audiences - /// Allowed issuers - /// Signing token provider - /// whether to validate username or not. - public AadAuthenticationSettings(string[] authorizedAppIds, string[] audiences, string[] issuers, IssuerSigningTokenProvider signingTokenProvider, bool validateUsername = false) - { - if (authorizedAppIds == null || authorizedAppIds.Length == 0) - { - throw new Exception("Authorized app Ids cannot be empty."); - } - - if (audiences == null || audiences.Length == 0) - { - throw new Exception("Audiences cannot be empty."); - } - - if (issuers == null || issuers.Length == 0) - { - throw new Exception("Issuers cannot be empty."); - } - - if (signingTokenProvider == null) - { - throw new Exception("Signing token provider cannot be null."); - } - - _authorizedAppIds = new HashSet(authorizedAppIds, StringComparer.OrdinalIgnoreCase); - _audiences = new HashSet(audiences, StringComparer.OrdinalIgnoreCase); - _issuers = new HashSet(issuers, StringComparer.OrdinalIgnoreCase); - _signingTokenProvider = signingTokenProvider; - _validateUsername = validateUsername; - } - - /// - /// Creates an AAD auth authenticator - /// - /// The main store the authenticator will be associated with. - public IGarnetAuthenticator CreateAuthenticator(StoreWrapper storeWrapper) - { - return new GarnetAadAuthenticator(_authorizedAppIds, _audiences, _issuers, _signingTokenProvider, _validateUsername, storeWrapper.logger); - } - - /// - /// Dispose impl - /// - /// Flag to run disposal logic - protected virtual void Dispose(bool disposing) - { - if (!_disposed) - { - if (disposing) - { - _signingTokenProvider?.Dispose(); - _signingTokenProvider = null; - } - - _disposed = true; - } - } - - /// - /// Dispose - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - } - - /// - /// ACL authentication settings - /// - public abstract class AclAuthenticationSettings : IAuthenticationSettings - { - /// - /// Location of a the ACL configuration file to load users from - /// - public readonly string AclConfigurationFile; - - /// - /// Default user password, in case aclConfiguration file is undefined or does not specify default password - /// - public readonly string DefaultPassword; - - /// - /// Creates and initializes new ACL authentication settings - /// - /// Location of the ACL configuration file - /// Optional default password, if not defined through aclConfigurationFile - public AclAuthenticationSettings(string aclConfigurationFile, string defaultPassword = "") - { - AclConfigurationFile = aclConfigurationFile; - DefaultPassword = defaultPassword; - } - - /// - /// Creates an ACL authenticator - /// - /// The main store the authenticator will be associated with. - public IGarnetAuthenticator CreateAuthenticator(StoreWrapper storeWrapper) - { - return CreateAuthenticatorInternal(storeWrapper); - } - - /// - /// Creates the internal implementation specific ACL authenticator. - /// - /// The main store the authenticator will be associated with. - /// IGarnetAuthenticator instance - protected abstract IGarnetAuthenticator CreateAuthenticatorInternal(StoreWrapper storeWrapper); - - - /// - /// Dispose - /// - public void Dispose() - { - // No-op - } - } - - /// - /// ACL authentication with AAD settings. - /// - public class AclAuthenticationPasswordSettings : AclAuthenticationSettings - { - - /// - /// Creates and initializes new ACL authentication settings - /// - /// Location of the ACL configuration file - /// Optional default password, if not defined through aclConfigurationFile - public AclAuthenticationPasswordSettings(string aclConfigurationFile, string defaultPassword = "") : base(aclConfigurationFile, defaultPassword) - { - } - - /// - /// Creates an ACL authenticator - /// - /// The main store the authenticator will be associated with. - - protected override IGarnetAuthenticator CreateAuthenticatorInternal(StoreWrapper storeWrapper) - { - return new GarnetAclWithPasswordAuthenticator(storeWrapper.accessControlList, storeWrapper.logger); - } - } - - - /// - /// ACL authentication with AAD settings - /// - public class AclAuthenticationAadSettings : AclAuthenticationSettings - { - - AadAuthenticationSettings _aadAuthenticationSettings; - - /// - /// Creates and initializes new ACL authentication settings - /// - /// Location of the ACL configuration file - /// Optional default password, if not defined through aclConfigurationFile - /// AAD settings used for authentication - public AclAuthenticationAadSettings(string aclConfigurationFile, string defaultPassword = "", AadAuthenticationSettings aadAuthenticationSettings = null) : base(aclConfigurationFile, defaultPassword) - { - _aadAuthenticationSettings = aadAuthenticationSettings; - } - - /// - /// Creates an ACL authenticator - /// - /// The main store the authenticator will be associated with. - - protected override IGarnetAuthenticator CreateAuthenticatorInternal(StoreWrapper storeWrapper) - { - return new GarnetAclWithAadAuthenticator(storeWrapper.accessControlList, _aadAuthenticationSettings.CreateAuthenticator(storeWrapper), storeWrapper.logger); - } - } -} \ No newline at end of file diff --git a/libs/server/Auth/Settings/AadAuthenticationSettings.cs b/libs/server/Auth/Settings/AadAuthenticationSettings.cs new file mode 100644 index 0000000000..4e65d4688a --- /dev/null +++ b/libs/server/Auth/Settings/AadAuthenticationSettings.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using Garnet.server.Auth.Aad; + +namespace Garnet.server.Auth.Settings +{ + /// + /// AAD auth settings + /// + public class AadAuthenticationSettings : IAuthenticationSettings + { + private readonly IReadOnlyCollection _authorizedAppIds; + private readonly IReadOnlyCollection _audiences; + private readonly IReadOnlyCollection _issuers; + private IssuerSigningTokenProvider _signingTokenProvider; + private bool _validateUsername; + private bool _disposed; + + /// + /// Constructor + /// + /// Allowed app Ids + /// Allowed audiences + /// Allowed issuers + /// Signing token provider + /// whether to validate username or not. + public AadAuthenticationSettings(string[] authorizedAppIds, string[] audiences, string[] issuers, IssuerSigningTokenProvider signingTokenProvider, bool validateUsername = false) + { + if (authorizedAppIds == null || authorizedAppIds.Length == 0) + { + throw new Exception("Authorized app Ids cannot be empty."); + } + + if (audiences == null || audiences.Length == 0) + { + throw new Exception("Audiences cannot be empty."); + } + + if (issuers == null || issuers.Length == 0) + { + throw new Exception("Issuers cannot be empty."); + } + + if (signingTokenProvider == null) + { + throw new Exception("Signing token provider cannot be null."); + } + + _authorizedAppIds = new HashSet(authorizedAppIds, StringComparer.OrdinalIgnoreCase); + _audiences = new HashSet(audiences, StringComparer.OrdinalIgnoreCase); + _issuers = new HashSet(issuers, StringComparer.OrdinalIgnoreCase); + _signingTokenProvider = signingTokenProvider; + _validateUsername = validateUsername; + } + + /// + /// Creates an AAD auth authenticator + /// + /// The main store the authenticator will be associated with. + public IGarnetAuthenticator CreateAuthenticator(StoreWrapper storeWrapper) + { + return new GarnetAadAuthenticator(_authorizedAppIds, _audiences, _issuers, _signingTokenProvider, _validateUsername, storeWrapper.logger); + } + + /// + /// Dispose impl + /// + /// Flag to run disposal logic + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _signingTokenProvider?.Dispose(); + _signingTokenProvider = null; + } + + _disposed = true; + } + } + + /// + /// Dispose + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/libs/server/Auth/Settings/AclAuthenticationAadSettings.cs b/libs/server/Auth/Settings/AclAuthenticationAadSettings.cs new file mode 100644 index 0000000000..f48ef9c14a --- /dev/null +++ b/libs/server/Auth/Settings/AclAuthenticationAadSettings.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Garnet.server.Auth.Settings +{ + /// + /// ACL authentication with AAD settings + /// + public class AclAuthenticationAadSettings : AclAuthenticationSettings + { + + AadAuthenticationSettings _aadAuthenticationSettings; + + /// + /// Creates and initializes new ACL authentication settings + /// + /// Location of the ACL configuration file + /// Optional default password, if not defined through aclConfigurationFile + /// AAD settings used for authentication + public AclAuthenticationAadSettings(string aclConfigurationFile, string defaultPassword = "", AadAuthenticationSettings aadAuthenticationSettings = null) : base(aclConfigurationFile, defaultPassword) + { + _aadAuthenticationSettings = aadAuthenticationSettings; + } + + /// + /// Creates an ACL authenticator + /// + /// The main store the authenticator will be associated with. + + protected override IGarnetAuthenticator CreateAuthenticatorInternal(StoreWrapper storeWrapper) + { + return new GarnetAclWithAadAuthenticator(storeWrapper.accessControlList, _aadAuthenticationSettings.CreateAuthenticator(storeWrapper), storeWrapper.logger); + } + } +} \ No newline at end of file diff --git a/libs/server/Auth/Settings/AclAuthenticationPasswordSettings.cs b/libs/server/Auth/Settings/AclAuthenticationPasswordSettings.cs new file mode 100644 index 0000000000..cfd4e770c0 --- /dev/null +++ b/libs/server/Auth/Settings/AclAuthenticationPasswordSettings.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Garnet.server.Auth.Settings +{ + /// + /// ACL authentication with AAD settings. + /// + public class AclAuthenticationPasswordSettings : AclAuthenticationSettings + { + + /// + /// Creates and initializes new ACL authentication settings + /// + /// Location of the ACL configuration file + /// Optional default password, if not defined through aclConfigurationFile + public AclAuthenticationPasswordSettings(string aclConfigurationFile, string defaultPassword = "") : base(aclConfigurationFile, defaultPassword) + { + } + + /// + /// Creates an ACL authenticator + /// + /// The main store the authenticator will be associated with. + + protected override IGarnetAuthenticator CreateAuthenticatorInternal(StoreWrapper storeWrapper) + { + return new GarnetAclWithPasswordAuthenticator(storeWrapper.accessControlList, storeWrapper.logger); + } + } +} \ No newline at end of file diff --git a/libs/server/Auth/Settings/AclAuthenticationSettings.cs b/libs/server/Auth/Settings/AclAuthenticationSettings.cs new file mode 100644 index 0000000000..52d5a870fb --- /dev/null +++ b/libs/server/Auth/Settings/AclAuthenticationSettings.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Garnet.server.Auth.Settings +{ + /// + /// ACL authentication settings + /// + public abstract class AclAuthenticationSettings : IAuthenticationSettings + { + /// + /// Location of a the ACL configuration file to load users from + /// + public readonly string AclConfigurationFile; + + /// + /// Default user password, in case aclConfiguration file is undefined or does not specify default password + /// + public readonly string DefaultPassword; + + /// + /// Creates and initializes new ACL authentication settings + /// + /// Location of the ACL configuration file + /// Optional default password, if not defined through aclConfigurationFile + public AclAuthenticationSettings(string aclConfigurationFile, string defaultPassword = "") + { + AclConfigurationFile = aclConfigurationFile; + DefaultPassword = defaultPassword; + } + + /// + /// Creates an ACL authenticator + /// + /// The main store the authenticator will be associated with. + public IGarnetAuthenticator CreateAuthenticator(StoreWrapper storeWrapper) + { + return CreateAuthenticatorInternal(storeWrapper); + } + + /// + /// Creates the internal implementation specific ACL authenticator. + /// + /// The main store the authenticator will be associated with. + /// IGarnetAuthenticator instance + protected abstract IGarnetAuthenticator CreateAuthenticatorInternal(StoreWrapper storeWrapper); + + + /// + /// Dispose + /// + public void Dispose() + { + // No-op + } + } +} \ No newline at end of file diff --git a/libs/server/Auth/Settings/AuthenticationSettings.cs b/libs/server/Auth/Settings/AuthenticationSettings.cs new file mode 100644 index 0000000000..a7565d928e --- /dev/null +++ b/libs/server/Auth/Settings/AuthenticationSettings.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using Garnet.server.Auth.Aad; + +namespace Garnet.server.Auth.Settings +{ + /// + /// Authentication mode + /// + public enum GarnetAuthenticationMode + { + /// + /// No auth - Garnet accepts any and all connections + /// + NoAuth, + + /// + /// Password - Garnet accepts connections with correct connection string + /// + Password, + + /// + /// AAD - Garnet accepts connection with correct AAD principal + /// In AAD mode, token may expire. Clients are expected to periodically refresh token with Garnet by running AUTH command. + /// + Aad, + + /// + /// ACL - Garnet validates new connections and commands against configured ACL users and access rules. + /// + ACL, + /// + /// ACL mode using Aad token instead of password. Here username is expected to be ObjectId or a valid Group's Object Id and token will be validated for claims. + /// + AclWithAad + } + + /// + /// Authentication settings + /// + public interface IAuthenticationSettings : IDisposable + { + /// + /// Create an authenticator using the current settings. + /// + /// The main store the authenticator will be associated with. + IGarnetAuthenticator CreateAuthenticator(StoreWrapper storeWrapper); + } +} \ No newline at end of file diff --git a/libs/server/Auth/Settings/NoAuthSettings.cs b/libs/server/Auth/Settings/NoAuthSettings.cs new file mode 100644 index 0000000000..2cc7a237e9 --- /dev/null +++ b/libs/server/Auth/Settings/NoAuthSettings.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Garnet.server.Auth.Settings +{ + /// + /// No auth settings + /// + public class NoAuthSettings : IAuthenticationSettings + { + /// + /// Creates a no auth authenticator + /// + /// The main store the authenticator will be associated with. + public IGarnetAuthenticator CreateAuthenticator(StoreWrapper storeWrapper) + { + return new GarnetNoAuthAuthenticator(); + } + + /// + /// Dispose + /// + public void Dispose() + { + // No-op + } + } +} \ No newline at end of file diff --git a/libs/server/Auth/Settings/PasswordAuthenticationSettings.cs b/libs/server/Auth/Settings/PasswordAuthenticationSettings.cs new file mode 100644 index 0000000000..0b2eda1cb2 --- /dev/null +++ b/libs/server/Auth/Settings/PasswordAuthenticationSettings.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; + +namespace Garnet.server.Auth.Settings +{ + /// + /// Password auth settings + /// + public class PasswordAuthenticationSettings : IAuthenticationSettings + { + private readonly byte[] _pwd; + + /// + /// Constructor + /// + /// The password + public PasswordAuthenticationSettings(string pwd) + { + if (string.IsNullOrEmpty(pwd)) + { + throw new Exception("Password cannot be null."); + } + _pwd = System.Text.Encoding.ASCII.GetBytes(pwd); + } + + /// + /// Creates a password auth authenticator + /// + /// The main store the authenticator will be associated with. + public IGarnetAuthenticator CreateAuthenticator(StoreWrapper storeWrapper) + { + return new GarnetPasswordAuthenticator(_pwd); + } + + /// + /// Dispose + /// + public void Dispose() + { + // No op + } + } +} \ No newline at end of file diff --git a/libs/server/Resp/ACLCommands.cs b/libs/server/Resp/ACLCommands.cs index 64a4522530..94661b4ac7 100644 --- a/libs/server/Resp/ACLCommands.cs +++ b/libs/server/Resp/ACLCommands.cs @@ -7,6 +7,7 @@ using Garnet.common; using Garnet.server.ACL; using Garnet.server.Auth; +using Garnet.server.Auth.Settings; using Microsoft.Extensions.Logging; namespace Garnet.server diff --git a/libs/server/Servers/GarnetServerOptions.cs b/libs/server/Servers/GarnetServerOptions.cs index 8e740136fc..1330deab4b 100644 --- a/libs/server/Servers/GarnetServerOptions.cs +++ b/libs/server/Servers/GarnetServerOptions.cs @@ -3,7 +3,7 @@ using System; using System.IO; -using Garnet.server.Auth; +using Garnet.server.Auth.Settings; using Garnet.server.TLS; using Microsoft.Extensions.Logging; using Tsavorite.core; diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 9007cc1703..5e26353eae 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; using Garnet.common; using Garnet.server.ACL; -using Garnet.server.Auth; +using Garnet.server.Auth.Settings; using Microsoft.Extensions.Logging; using Tsavorite.core; diff --git a/test/Garnet.test.cluster/ClusterAadAuthTests.cs b/test/Garnet.test.cluster/ClusterAadAuthTests.cs index 0e7b551d01..81dabdc035 100644 --- a/test/Garnet.test.cluster/ClusterAadAuthTests.cs +++ b/test/Garnet.test.cluster/ClusterAadAuthTests.cs @@ -5,15 +5,13 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; -using System.Text; -using System.Threading.Tasks; -using Garnet.server.Auth; -using Garnet.server.Auth.Aad; +using Garnet.server.Auth.Settings; using Microsoft.IdentityModel.Tokens; using NUnit.Framework; namespace Garnet.test.cluster { + [TestFixture] [NonParallelizable] class ClusterAadAuthTests @@ -22,6 +20,8 @@ class ClusterAadAuthTests readonly HashSet monitorTests = []; + private const string issuer = "https://sts.windows.net/975f013f-7f24-47e8-a7d3-abc4752bf346/"; + [SetUp] public void Setup() { @@ -41,7 +41,7 @@ public void ValidateClusterAuthWithObjectId() { var nodes = 2; var audience = Guid.NewGuid().ToString(); - JwtTokenGenerator tokenGenerator = new JwtTokenGenerator(audience); + JwtTokenGenerator tokenGenerator = new JwtTokenGenerator(issuer, audience); var appId = Guid.NewGuid().ToString(); var objId = Guid.NewGuid().ToString(); @@ -51,7 +51,7 @@ public void ValidateClusterAuthWithObjectId() new Claim("appid", appId), new Claim("http://schemas.microsoft.com/identity/claims/objectidentifier",objId), }; - var authSettings = new AadAuthenticationSettings([appId], [audience], [audience], IssuerSigningTokenProvider.Create(new List { tokenGenerator.SecurityKey }, context.logger), true); + var authSettings = new AadAuthenticationSettings([appId], [audience], [issuer], new MockIssuerSigningTokenProvider(new List { tokenGenerator.SecurityKey }, context.logger), true); var token = tokenGenerator.CreateToken(tokenClaims, DateTime.Now.AddMinutes(10)); ValidateConnectionsWithToken(objId, token, nodes, authSettings); @@ -63,7 +63,7 @@ public void ValidateClusterAuthWithGroupOid() { var nodes = 2; var audience = Guid.NewGuid().ToString(); - JwtTokenGenerator tokenGenerator = new JwtTokenGenerator(audience); + JwtTokenGenerator tokenGenerator = new JwtTokenGenerator(issuer, audience); var appId = Guid.NewGuid().ToString(); var objId = Guid.NewGuid().ToString(); @@ -75,7 +75,7 @@ public void ValidateClusterAuthWithGroupOid() new Claim("http://schemas.microsoft.com/identity/claims/objectidentifier", objId), new Claim("groups", string.Join(',', groupIds)), }; - var authSettings = new AadAuthenticationSettings([appId], [audience], [audience], IssuerSigningTokenProvider.Create(new List { tokenGenerator.SecurityKey }, context.logger), true); + var authSettings = new AadAuthenticationSettings([appId], [audience], [issuer], new MockIssuerSigningTokenProvider(new List { tokenGenerator.SecurityKey }, context.logger), true); var token = tokenGenerator.CreateToken(tokenClaims, DateTime.Now.AddMinutes(10)); ValidateConnectionsWithToken(groupIds.First(), token, nodes, authSettings); } diff --git a/test/Garnet.test.cluster/ClusterTestContext.cs b/test/Garnet.test.cluster/ClusterTestContext.cs index 6579ed26c5..c6477fde38 100644 --- a/test/Garnet.test.cluster/ClusterTestContext.cs +++ b/test/Garnet.test.cluster/ClusterTestContext.cs @@ -9,7 +9,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Garnet.server.Auth; +using Garnet.server.Auth.Settings; using Microsoft.Extensions.Logging; using NUnit.Framework; using StackExchange.Redis; diff --git a/test/Garnet.test.cluster/JwtTokenGenerator.cs b/test/Garnet.test.cluster/JwtTokenHelpers.cs similarity index 68% rename from test/Garnet.test.cluster/JwtTokenGenerator.cs rename to test/Garnet.test.cluster/JwtTokenHelpers.cs index d41a30d3e4..10532a57bd 100644 --- a/test/Garnet.test.cluster/JwtTokenGenerator.cs +++ b/test/Garnet.test.cluster/JwtTokenHelpers.cs @@ -8,14 +8,25 @@ using System.Security.Claims; using System.Text; using System.Threading.Tasks; +using Garnet.server.Auth.Aad; +using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; namespace Garnet.test.cluster { + internal class MockIssuerSigningTokenProvider : IssuerSigningTokenProvider + { + internal MockIssuerSigningTokenProvider(IReadOnlyCollection signingTokens, ILogger logger = null) : base(string.Empty, signingTokens, false, logger) + { } + + } + internal class JwtTokenGenerator { private readonly string Issuer; + private readonly string Audience; + // Our random signing key - used to sign and validate the tokens public SecurityKey SecurityKey { get; } @@ -25,16 +36,17 @@ internal class JwtTokenGenerator // the token handler we'll use to actually issue tokens public readonly JwtSecurityTokenHandler JwtSecurityTokenHandler = new(); - internal JwtTokenGenerator(string issuer) + internal JwtTokenGenerator(string issuer, string audience) { Issuer = issuer; + Audience = audience; SecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Test secret key for authentication and signing the token to be generated")); SigningCredentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256); } internal string CreateToken(List claims, DateTime expiryTime) { - return JwtSecurityTokenHandler.WriteToken(new JwtSecurityToken(Issuer, Issuer, claims, expires: expiryTime, signingCredentials: SigningCredentials)); + return JwtSecurityTokenHandler.WriteToken(new JwtSecurityToken(Issuer, Audience, claims, expires: expiryTime, signingCredentials: SigningCredentials)); } } diff --git a/test/Garnet.test/TestUtils.cs b/test/Garnet.test/TestUtils.cs index 786285f7cb..3b49fe2fe2 100644 --- a/test/Garnet.test/TestUtils.cs +++ b/test/Garnet.test/TestUtils.cs @@ -15,7 +15,7 @@ using Garnet.client; using Garnet.common; using Garnet.server; -using Garnet.server.Auth; +using Garnet.server.Auth.Settings; using Garnet.server.TLS; using Microsoft.Extensions.Logging; using NUnit.Framework;