diff --git a/src/Core/Resolvers/MsSqlQueryExecutor.cs b/src/Core/Resolvers/MsSqlQueryExecutor.cs index b4f84757b3..2078b4f1c5 100644 --- a/src/Core/Resolvers/MsSqlQueryExecutor.cs +++ b/src/Core/Resolvers/MsSqlQueryExecutor.cs @@ -516,6 +516,7 @@ public override string GetSessionParamsQuery(HttpContext? httpContext, IDictiona // Counter to generate different param name for each of the sessionParam. IncrementingInteger counter = new(); + const string SESSION_KEY_NAME = $"{BaseQueryStructure.PARAM_NAME_PREFIX}session_key"; const string SESSION_PARAM_NAME = $"{BaseQueryStructure.PARAM_NAME_PREFIX}session_param"; StringBuilder sessionMapQuery = new(); @@ -528,10 +529,14 @@ public override string GetSessionParamsQuery(HttpContext? httpContext, IDictiona foreach ((string claimType, string claimValue) in sessionParams) { + string keyName = $"{SESSION_KEY_NAME}{counter.Current()}"; + parameters.Add(keyName, new(claimType)); + string paramName = $"{SESSION_PARAM_NAME}{counter.Next()}"; parameters.Add(paramName, new(claimValue)); + // Append statement to set read only param value - can be set only once for a connection. - string statementToSetReadOnlyParam = "EXEC sp_set_session_context " + $"'{claimType}', " + paramName + ", @read_only = 0;"; + string statementToSetReadOnlyParam = "EXEC sp_set_session_context " + keyName + ", " + paramName + ", @read_only = 0;"; sessionMapQuery = sessionMapQuery.Append(statementToSetReadOnlyParam); } } diff --git a/src/Service.Tests/Authentication/EasyAuthAuthenticationUnitTests.cs b/src/Service.Tests/Authentication/EasyAuthAuthenticationUnitTests.cs index 35ef925dab..40ea59fa1a 100644 --- a/src/Service.Tests/Authentication/EasyAuthAuthenticationUnitTests.cs +++ b/src/Service.Tests/Authentication/EasyAuthAuthenticationUnitTests.cs @@ -2,14 +2,18 @@ // Licensed under the MIT License. #nullable enable +using System; +using System.IO; using System.Collections.Generic; using System.Linq; using System.Net; +using System.Net.Http; using System.Security.Claims; using System.Threading.Tasks; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Core.AuthenticationHelpers; using Azure.DataApiBuilder.Core.Authorization; +using Azure.DataApiBuilder.Core.Configurations; using Azure.DataApiBuilder.Service.Tests.Authentication.Helpers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; @@ -325,7 +329,6 @@ public async Task TestValidStaticWebAppsEasyAuthTokenWithAnonymousRoleOnly() DisplayName = "Anonymous role - X-MS-API-ROLE is not honored")] [DataRow(true, "author", DisplayName = "Authenticated role - existing X-MS-API-ROLE is honored")] - [TestMethod] public async Task TestClientRoleHeaderPresence(bool addAuthenticated, string clientRoleHeader) { string generatedToken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken(addAuthenticated); @@ -344,6 +347,49 @@ await SendRequestAndGetHttpContextState( ignoreCase: true); } + /// + /// Tests we ensure that an invalid query in the EasyAuth header does not result in a successful request. + /// + [TestMethod] + public async Task TestInvalidQueryInHeader() + { + string header = @" + { + ""auth_typ"":""aad"", + ""claims"":[ + { + ""typ"":""x', N'v';INSERT INTO authors (id, name, birthdate) VALUES (10001, 'Hidden Author', '2001-01-01');--"", + ""val"":""x"" + } + ], + ""UserRoles"":[""authenticated""] + }"; + + string generatedToken = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(header)); + + const string SESSION_CONFIG = $"session-context-config.{TestCategory.MSSQL}.json"; + SetupSessionContextConfig(SESSION_CONFIG); + + string[] args = [$"--ConfigFileName={SESSION_CONFIG}"]; + using TestServer server = new(Program.CreateWebHostBuilder(args)); + using HttpClient client = server.CreateClient(); + + HttpRequestMessage requestWithHeader = new(HttpMethod.Get, "api/Author"); + requestWithHeader.Headers.Add(AuthenticationOptions.CLIENT_PRINCIPAL_HEADER, generatedToken); + requestWithHeader.Headers.Add(AuthorizationResolver.CLIENT_ROLE_HEADER, "authenticated"); + HttpResponseMessage responseWithHeader = await client.SendAsync(requestWithHeader); + Assert.AreEqual(expected: HttpStatusCode.OK, actual: responseWithHeader.StatusCode); + + HttpRequestMessage request = new(HttpMethod.Get, $"api/Author/id/10001"); + HttpResponseMessage response = await client.SendAsync(request); + string responseBody = await response.Content.ReadAsStringAsync(); + + Assert.IsFalse(responseBody.Contains($"\"id\":10001"), + "The GET request should not return the invalid row."); + Assert.IsFalse(responseBody.Contains("Hidden Author"), + "The GET request should not return any information related to the invalid row."); + } + /// /// - Ensures an invalid EasyAuth header payload results in HTTP 401 Unauthorized response /// A correctly configured EasyAuth environment guarantees an EasyAuth payload for authenticated requests. @@ -429,6 +475,40 @@ public static async Task SendRequestAndGetHttpContextState( context.Request.Scheme = "https"; }); } + + /// + /// Writes a MsSql runtime config that uses StaticWebApps EasyAuth and enables + /// set-session-context so requests exercise MsSqlQueryExecutor.GetSessionParamsQuery. + /// + private static void SetupSessionContextConfig(string configFileName) + { + TestHelper.SetupDatabaseEnvironment(TestCategory.MSSQL); + RuntimeConfigProvider configProvider = + TestHelper.GetRuntimeConfigProvider(TestHelper.GetRuntimeConfigLoader()); + RuntimeConfig config = configProvider.GetConfig(); + + RuntimeConfig updatedConfig = config with + { + DataSource = config.DataSource! with + { + Options = new Dictionary + { + // Matches MsSqlOptions.SetSessionContext (hyphenated naming policy). + { "set-session-context", true } + } + }, + Runtime = config.Runtime! with + { + Host = config.Runtime.Host! with + { + Authentication = new AuthenticationOptions( + Provider: EasyAuthType.AppService.ToString(), Jwt: null) + } + } + }; + + File.WriteAllText(configFileName, updatedConfig.ToJson()); + } #endregion } }