From 21f94403ca51677ce63c09b3b305a9129f282514 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Mon, 21 Sep 2026 16:40:22 +0530 Subject: [PATCH] Fix #3541: resolve Custom JWT provider to Bearer auth scheme Custom (and any non out-of-box JWT) auth providers registered the JWT handler under the Bearer scheme but ClientRoleHeaderAuthenticationMiddleware resolved the request scheme to the unregistered 'OAuthAuthentication', causing AuthenticateAsync to throw 'No authentication handler is registered for the scheme OAuthAuthentication' on every REST/GraphQL/OpenAPI request. Resolve all JWT-configured providers to JwtBearerDefaults.AuthenticationScheme and remove the now-unused GenericOAuthDefaults. Adds regression test covering Custom/AzureAD/EntraID. --- ...lientRoleHeaderAuthenticationMiddleware.cs | 16 +++--- .../GenericOAuthDefaults.cs | 12 ----- .../JwtTokenAuthenticationUnitTests.cs | 51 +++++++++++++++++-- 3 files changed, 54 insertions(+), 25 deletions(-) delete mode 100644 src/Core/AuthenticationHelpers/GenericOAuthDefaults.cs diff --git a/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs b/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs index fa7fdc9a25..ba7c714243 100644 --- a/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs +++ b/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs @@ -197,17 +197,15 @@ private static string ResolveConfiguredAuthNScheme(string? configuredProviderNam { return UnauthenticatedAuthenticationDefaults.AUTHENTICATIONSCHEME; } - else if (string.Equals(configuredProviderName, SupportedAuthNProviders.AZURE_AD, StringComparison.OrdinalIgnoreCase) || - string.Equals(configuredProviderName, SupportedAuthNProviders.ENTRA_ID, StringComparison.OrdinalIgnoreCase)) - { - return JwtBearerDefaults.AuthenticationScheme; - } else { - // Changing this value is a breaking change because non-out of box - // authentication provider names supplied in dab-config.json indicate - // that JWT bearer authentication should be used. - return GenericOAuthDefaults.AUTHENTICATIONSCHEME; + // Every non-EasyAuth/Simulator/Unauthenticated provider (AzureAD, EntraID, and any + // custom OAuth/JWT provider such as "Custom") is authenticated via JWT bearer. The JWT + // handler is always registered under JwtBearerDefaults.AuthenticationScheme ("Bearer") + // in Startup's ConfigureAuthentication/ConfigureAuthenticationV2, so the resolved scheme + // must match that registration - otherwise AuthenticateAsync throws + // "No authentication handler is registered for the scheme ...". + return JwtBearerDefaults.AuthenticationScheme; } } } diff --git a/src/Core/AuthenticationHelpers/GenericOAuthDefaults.cs b/src/Core/AuthenticationHelpers/GenericOAuthDefaults.cs deleted file mode 100644 index 0faf2b3085..0000000000 --- a/src/Core/AuthenticationHelpers/GenericOAuthDefaults.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Azure.DataApiBuilder.Core.AuthenticationHelpers; - -/// -/// Authentication Scheme name for generic OAuth providers. -/// -public class GenericOAuthDefaults -{ - public const string AUTHENTICATIONSCHEME = "OAuthAuthentication"; -} diff --git a/src/Service.Tests/Authentication/JwtTokenAuthenticationUnitTests.cs b/src/Service.Tests/Authentication/JwtTokenAuthenticationUnitTests.cs index 12a0b2ebd0..ab15d2d879 100644 --- a/src/Service.Tests/Authentication/JwtTokenAuthenticationUnitTests.cs +++ b/src/Service.Tests/Authentication/JwtTokenAuthenticationUnitTests.cs @@ -87,6 +87,46 @@ await SendRequestAndGetHttpContextState( ignoreCase: true); } + /// + /// Regression test for https://github.com/Azure/data-api-builder/issues/3541 + /// A JWT-configured provider whose name is not an out-of-box provider (e.g. "Custom") must + /// resolve to the same "Bearer" scheme the JWT handler is registered under. Previously this + /// resolved to the unregistered "OAuthAuthentication" scheme, causing AuthenticateAsync to + /// throw "No authentication handler is registered for the scheme 'OAuthAuthentication'". + /// + [DataTestMethod] + [DataRow("Custom", DisplayName = "Custom JWT provider authenticates via the Bearer scheme")] + [DataRow("AzureAD", DisplayName = "AzureAD JWT provider authenticates via the Bearer scheme")] + [DataRow("EntraID", DisplayName = "EntraID JWT provider authenticates via the Bearer scheme")] + [TestMethod] + public async Task TestValidToken_JwtConfiguredProviders(string provider) + { + RsaSecurityKey key = new(RSA.Create(2048)); + string token = CreateJwt( + audience: AUDIENCE, + issuer: LOCAL_ISSUER, + notBefore: DateTime.UtcNow.AddDays(-1), + expirationTime: DateTime.UtcNow.AddDays(1), + signingKey: key + ); + + HttpContext postMiddlewareContext = + await SendRequestAndGetHttpContextState( + key, + token, + clientRoleHeader: null, + provider: provider); + + Assert.IsTrue(postMiddlewareContext.User.Identity.IsAuthenticated); + Assert.AreEqual( + expected: (int)HttpStatusCode.OK, + actual: postMiddlewareContext.Response.StatusCode); + Assert.AreEqual( + expected: AuthorizationType.Authenticated.ToString(), + actual: postMiddlewareContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER], + ignoreCase: true); + } + /// /// Test to validate that the user request is treated with anonymous role when /// the jwt token is missing. @@ -302,15 +342,17 @@ public async Task TestInvalidToken_NoSignature() /// and configures Authentication options with passed in SecurityKey /// /// + /// Runtime configured identity provider name (e.g. "AzureAD" or a + /// custom OAuth/JWT provider such as "Custom"). All resolve to JWT bearer authentication. /// IHost - private static async Task CreateWebHostCustomIssuer(SecurityKey key) + private static async Task CreateWebHostCustomIssuer(SecurityKey key, string provider = "AzureAD") { // Setup RuntimeConfigProvider object for the pipeline. MockFileSystem fileSystem = new(); FileSystemRuntimeConfigLoader fileSystemRuntimeConfigLoader = new(new MockFileSystem()); AuthenticationOptions authOptions = new() { - Provider = "AzureAD" + Provider = provider }; RuntimeConfig runtimeConfig = RuntimeConfigAuthHelper.CreateTestConfigWithAuthNProvider(authOptions); @@ -384,9 +426,10 @@ private static async Task CreateWebHostCustomIssuer(SecurityKey key) private static async Task SendRequestAndGetHttpContextState( SecurityKey key, string token, - string clientRoleHeader = null) + string clientRoleHeader = null, + string provider = "AzureAD") { - using IHost host = await CreateWebHostCustomIssuer(key); + using IHost host = await CreateWebHostCustomIssuer(key, provider); TestServer server = host.GetTestServer(); return await server.SendAsync(context =>