diff --git a/src/Service.Tests/AuthTestHelper.cs b/src/Service.Tests/AuthTestHelper.cs index 80d7aa3a70..cd1e91422c 100644 --- a/src/Service.Tests/AuthTestHelper.cs +++ b/src/Service.Tests/AuthTestHelper.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Collections.Generic; using System.Security.Claims; @@ -13,8 +14,14 @@ internal static class AuthTestHelper /// /// Creates a mocked EasyAuth token, namely, the value of the header injected by EasyAuth. /// + /// Defines the ClaimType of the claim used for the return value of Identity.Name + /// Defines the ClaimType of the claim used for the return value of ClaimsPrincpal.IsInRole(roleName) /// A Base64 encoded string of a serialized EasyAuthClientPrincipal object - public static string CreateAppServiceEasyAuthToken() + /// + /// + public static string CreateAppServiceEasyAuthToken( + string? nameClaimType = ClaimTypes.Name, + string? roleClaimType = ClaimTypes.Role) { AppServiceClaim emailClaim = new() { @@ -25,26 +32,56 @@ public static string CreateAppServiceEasyAuthToken() AppServiceClaim roleClaimAnonymous = new() { Val = "Anonymous", - Typ = ClaimTypes.Role + Typ = roleClaimType }; AppServiceClaim roleClaimAuthenticated = new() { Val = "Authenticated", + Typ = roleClaimType + }; + + AppServiceClaim roleClaimShortNameClaimType = new() + { + Val = "RoleShortClaimType", + Typ = "roles" + }; + + AppServiceClaim roleClaimUriClaimType = new() + { + Val = "RoleUriClaimType", Typ = ClaimTypes.Role }; - List claims = new(); - claims.Add(emailClaim); - claims.Add(roleClaimAnonymous); - claims.Add(roleClaimAuthenticated); + AppServiceClaim nameShortClaimType = new() + { + Val = "NameShortClaimType", + Typ = "unique_name" + }; + + AppServiceClaim nameUriClaimType = new() + { + Val = "NameUriClaimType", + Typ = ClaimTypes.Name + }; + + List claims = new() + { + emailClaim, + roleClaimAnonymous, + roleClaimAuthenticated, + roleClaimShortNameClaimType, + roleClaimUriClaimType, + nameShortClaimType, + nameUriClaimType + }; AppServiceClientPrincipal token = new() { Auth_typ = "aad", - Name_typ = "Apple Banana", + Name_typ = nameClaimType, Claims = claims, - Role_typ = ClaimTypes.Role + Role_typ = roleClaimType }; string serializedToken = JsonSerializer.Serialize(value: token); @@ -60,8 +97,8 @@ public static string CreateAppServiceEasyAuthToken() /// A Base64 encoded string of a serialized StaticWebAppsClientPrincipal object public static string CreateStaticWebAppsEasyAuthToken( bool addAuthenticated = true, - string specificRole = null, - IEnumerable claims = null) + string? specificRole = null, + IEnumerable? claims = null) { // The anonymous role is present in all requests sent to Static Web Apps or AppService endpoints. List roles = new() diff --git a/src/Service.Tests/Authentication/EasyAuthAuthenticationUnitTests.cs b/src/Service.Tests/Authentication/EasyAuthAuthenticationUnitTests.cs index c55f808dce..fbf29323bc 100644 --- a/src/Service.Tests/Authentication/EasyAuthAuthenticationUnitTests.cs +++ b/src/Service.Tests/Authentication/EasyAuthAuthenticationUnitTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Security.Claims; using System.Threading.Tasks; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.Authorization; @@ -22,16 +23,16 @@ namespace Azure.DataApiBuilder.Service.Tests.Authentication [TestClass] public class EasyAuthAuthenticationUnitTests { - #region Positive Tests + #region Tests /// - /// Ensures a valid AppService EasyAuth header/value does NOT result in HTTP 401 Unauthenticated response. - /// 403 is okay, as it indicates authorization level failure, not authentication. - /// When an authorization header is sent, it contains an invalid value, if the runtime returns an error - /// then there is improper JWT validation occurring. + /// Ensures a valid AppService EasyAuth header/value results in HTTP 200 or HTTP 403. + /// HTTP 401 will not occur when EasyAuth is correctly configured (AppService environment and runtime configuration). + /// When EasyAuth is configured and an authorization header is sent, the authorization header should be ignored + /// and zero token validation errors should be observed. /// [DataTestMethod] - [DataRow(false, DisplayName = "Valid AppService EasyAuth header only")] - [DataRow(true, DisplayName = "Valid AppService EasyAuth header and authorization header")] + [DataRow(false, DisplayName = "Valid AppService EasyAuth payload - 200")] + [DataRow(true, DisplayName = "Valid AppService EasyAuth header and authorization header - 200")] [TestMethod] public async Task TestValidAppServiceEasyAuthToken(bool sendAuthorizationHeader) { @@ -40,6 +41,7 @@ public async Task TestValidAppServiceEasyAuthToken(bool sendAuthorizationHeader) generatedToken, EasyAuthType.AppService, sendAuthorizationHeader); + Assert.IsNotNull(postMiddlewareContext.User.Identity); Assert.IsTrue(postMiddlewareContext.User.Identity.IsAuthenticated); Assert.AreEqual(expected: (int)HttpStatusCode.OK, @@ -50,6 +52,105 @@ public async Task TestValidAppServiceEasyAuthToken(bool sendAuthorizationHeader) ignoreCase: true); } + /// + /// Tests that the value returned by Identity.Name comes from the claim of type nameClaimType, + /// which reflects correct AppService EasyAuth parsing into a ClaimsIdentity object used to + /// create a ClaimsPrincipal object. + /// + /// Expected name to be returned by Identity.Name + /// Defines the ClaimType of the claim used for the return value of Identity.Name + /// + /// + [DataTestMethod] + [DataRow("NameShortClaimType", "unique_name", DisplayName = "Identity.Name from custom claim name type")] + [DataRow("NameUriClaimType", ClaimTypes.Name, DisplayName = "Identity.Name from URI claim name type")] + [DataRow("NameUriClaimType", null, DisplayName = "Identity.Name from default URI claim name type")] + public async Task TestNameClaimTypeAppServiceEasyAuthToken(string? name, string? nameClaimType) + { + // Generated token has the following relevant claims: + // Type | Value + // NameShortClaimType | unique_name + // NameUriClaimType | ClaimTypes.Name + string generatedToken = AuthTestHelper.CreateAppServiceEasyAuthToken(nameClaimType: nameClaimType); + HttpContext postMiddlewareContext = await SendRequestAndGetHttpContextState( + generatedToken, + EasyAuthType.AppService + ); + + Assert.IsNotNull(postMiddlewareContext.User.Identity); + Assert.IsTrue(postMiddlewareContext.User.Identity.IsAuthenticated); + Assert.AreEqual(expected: name, actual: postMiddlewareContext.User.Identity.Name); + } + + /// + /// Tests that the value returned by ClaimsPrincipal.IsInRole comes from the claim of type roleClaimType, + /// which reflects correct AppService EasyAuth parsing into a ClaimsIdentity object used to + /// create a ClaimsPrincipal object. + /// + /// User expected to be in role. + /// Name of role to check in role membership query. + /// Defines the ClaimType of the claim used for the return value of ClaimsPrincpal.IsInRole(roleName) + /// + /// + [DataTestMethod] + [DataRow(true, "RoleShortClaimType", "roles")] + [DataRow(false, "RoleUriClaimType", "roles")] + [DataRow(false, "RoleShortClaimType", ClaimTypes.Role)] + [DataRow(true, "RoleUriClaimType", ClaimTypes.Role)] + public async Task TestRoleClaimTypeAppServiceEasyAuthToken(bool isInRole, string roleName, string roleClaimType) + { + // Generated token has the following relevant claims: + // Type | Value + // RoleShortClaimType | roles + // RoleUriClaimType | ClaimTypes.Role + string generatedToken = AuthTestHelper.CreateAppServiceEasyAuthToken(roleClaimType: roleClaimType); + HttpContext postMiddlewareContext = await SendRequestAndGetHttpContextState( + generatedToken, + EasyAuthType.AppService + ); + + Assert.IsNotNull(postMiddlewareContext.User.Identity); + Assert.IsTrue(postMiddlewareContext.User.Identity.IsAuthenticated); + Assert.AreEqual(expected: isInRole, actual: postMiddlewareContext.User.IsInRole(roleName)); + } + + /// + /// Invalid AppService EasyAuth payloads elicit a 401 Unauthorized response, indicating failed authentication. + /// + [DataTestMethod] + [DataRow("", DisplayName = "Empty JSON not serializable to AppServiceClientPrincipal")] + [DataRow("eyJtZXNzYWdlIjogImhlbGxvIHdvcmxkIn0=", DisplayName = "JSON not serializable to AppServiceClientPrincipal")] + [DataRow("aGVsbG8sIHdvcmxkIQ==", DisplayName = "Non-JSON Base64 encoded string not serializable to AppServiceClientPrincipal")] + [DataRow("eyJhdXRoX3R5cCI6IiIsIm5hbWVfdHlwIjoidW5pcXVlX25hbWUiLCJyb2xlX3R5cCI6InJvbGVzIn0=", DisplayName = "Missing value for property Auth_typ")] + [DataRow("eyJhdXRoX3R5cCI6IG51bGwsIm5hbWVfdHlwIjoidW5pcXVlX25hbWUiLCJyb2xlX3R5cCI6InJvbGVzIn0=", DisplayName = "Null value for property Auth_typ")] + [DataRow("eyJhdXRoX3R5cCI6ICIiLCJuYW1lX3R5cCI6InVuaXF1ZV9uYW1lIiwicm9sZV90eXAiOiJyb2xlcyJ9", DisplayName = "Empty value for property Auth_typ")] + public async Task TestInvalidAppServiceEasyAuthToken(string easyAuthPayload) + { + HttpContext postMiddlewareContext = await SendRequestAndGetHttpContextState(token: easyAuthPayload, EasyAuthType.AppService); + + Assert.AreEqual(expected: (int)HttpStatusCode.Unauthorized, actual: postMiddlewareContext.Response.StatusCode); + Assert.IsNotNull(postMiddlewareContext.User.Identity); + Assert.AreEqual(expected: false, actual: postMiddlewareContext.User.Identity.IsAuthenticated); + } + + /// + /// Ensures authentication fails when no EasyAuth header is present because + /// a correctly configured EasyAuth environment guarantees that only authenticated requests + /// will contain an EasyAuth header. + /// + /// AppService/StaticWebApps + [DataTestMethod] + [DataRow(EasyAuthType.AppService)] + [DataRow(EasyAuthType.StaticWebApps)] + public async Task TestMissingEasyAuthHeader(EasyAuthType easyAuthType) + { + HttpContext postMiddlewareContext = await SendRequestAndGetHttpContextState(token: null, easyAuthType); + + Assert.AreEqual(expected: (int)HttpStatusCode.OK, actual: postMiddlewareContext.Response.StatusCode); + Assert.IsNotNull(postMiddlewareContext.User.Identity); + Assert.IsFalse(postMiddlewareContext.User.Identity.IsAuthenticated); + } + /// /// Ensures a valid StaticWebApps EasyAuth header/value does NOT result in HTTP 401 Unauthorized response. /// 403 is okay, as it indicates authorization level failure, not authentication. @@ -199,6 +300,8 @@ await SendRequestAndGetHttpContextState( generatedToken, EasyAuthType.StaticWebApps, clientRoleHeader: clientRoleHeader); + + // Validate state of HttpContext after being processed by authentication middleware. Assert.IsNotNull(postMiddlewareContext.User.Identity); Assert.AreEqual(expected: addAuthenticated, postMiddlewareContext.User.Identity.IsAuthenticated); Assert.AreEqual(expected: (int)HttpStatusCode.OK, actual: postMiddlewareContext.Response.StatusCode); @@ -208,58 +311,57 @@ await SendRequestAndGetHttpContextState( } /// - /// - Ensures an invalid/no EasyAuth header/value results in HTTP 200 OK response - /// but with the X-MS-API-ROLE assigned to be anonymous. + /// - Ensures an invalid EasyAuth header payload results in HTTP 401 Unauthorized response + /// A correctly configured EasyAuth environment guarantees an EasyAuth payload for authenticated requests. + /// - Ensures a missing EasyAuth header results in HTTP OK and User.IsAuthenticated == false. /// - Also, validate that if other auth headers are present (Authorization Bearer token), that it is never considered /// when the runtime is configured for EasyAuth authentication. /// - /// EasyAuth header value - /// + /// EasyAuth header value [DataTestMethod] - [DataRow("", DisplayName = "No EasyAuth header value provided")] - [DataRow("ey==", DisplayName = "Corrupt EasyAuth header value provided")] - [DataRow(null, DisplayName = "No EasyAuth header provided")] - [DataRow("", true, DisplayName = "No EasyAuth header value provided, include authorization header")] + [DataRow("", DisplayName = "No EasyAuth payload -> 401 Unauthorized")] + [DataRow("ey==", DisplayName = "Invalid EasyAuth payload -> 401 Unauthorized")] + [DataRow(null, DisplayName = "No EasyAuth header provided -> 200 OK, Anonymous request")] + [DataRow("", true, DisplayName = "No EasyAuth payload, include authorization header")] [DataRow("ey==", true, DisplayName = "Corrupt EasyAuth header value provided, include authorization header")] - [DataRow(null, true, DisplayName = "No EasyAuth header provided, include authorization header")] + [DataRow(null, true, DisplayName = "No EasyAuth header provided, include authorization header -> 200 OK, Anonymous request")] [TestMethod] - public async Task TestInvalidEasyAuthToken(string token, bool sendAuthorizationHeader = false) + public async Task TestInvalidStaticWebAppsEasyAuthToken(string easyAuthPayload, bool sendAuthorizationHeader = false) { HttpContext postMiddlewareContext = await SendRequestAndGetHttpContextState( - token, + easyAuthPayload, EasyAuthType.StaticWebApps, sendAuthorizationHeader); + + // Validate state of HttpContext after being processed by authentication middleware. Assert.IsNotNull(postMiddlewareContext.User.Identity); Assert.IsFalse(postMiddlewareContext.User.Identity.IsAuthenticated); - Assert.AreEqual(expected: (int)HttpStatusCode.OK, actual: postMiddlewareContext.Response.StatusCode); - Assert.AreEqual(expected: AuthorizationType.Anonymous.ToString(), - actual: postMiddlewareContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER], - ignoreCase: true); + + // A missing EasyAuth header results in an anonymous request. + int expectedStatusCode = (easyAuthPayload is not null) ? (int)HttpStatusCode.Unauthorized : (int)HttpStatusCode.OK; + Assert.AreEqual(expected: expectedStatusCode, actual: postMiddlewareContext.Response.StatusCode); + string expectedResolvedRoleHeader = (easyAuthPayload is not null) ? string.Empty : AuthorizationResolver.ROLE_ANONYMOUS; + string actualResolvedRoleHeader = postMiddlewareContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER].ToString(); + Assert.AreEqual(expected: expectedResolvedRoleHeader, actual: actualResolvedRoleHeader, ignoreCase: true); } /// /// Test to validate that the request is appropriately treated as anonymous/authenticated - /// in development mode depending on the value feature switch we have in the config file. + /// in development mode depending on the value feature flag we have in the config file. + /// (Enabled/Disabled) in test DisplayName indicates state of feature flag. /// - /// Boolean value indicating whether to treat the - /// request as authenticated by default. + /// Boolean value indicating whether to turn on feature flag + /// used to treat requests as authenticated by default. /// Expected value of X-MS-API-ROLE header. /// Value of X-MS-API-ROLE header specified in request. - /// [DataTestMethod] - [DataRow(true, "Authenticated", null, - DisplayName = "EasyAuth- Treat request as authenticated in development mode")] - [DataRow(false, "Anonymous", null, - DisplayName = "EasyAuth- Treat request as anonymous in development mode")] - [DataRow(true, "author", "author", - DisplayName = "EasyAuth- Treat request as authenticated in development mode " + - "and honor the clientRoleHeader")] - [DataRow(true, "Anonymous", "Anonymous", - DisplayName = "EasyAuth- Treat request as authenticated in development mode " + - "and honor the clientRoleHeader even when specified as anonymous")] - public async Task TestAuthenticatedRequestInDevelopmentMode( - bool treatDevModeRequestAsAuthenticated, + [DataRow(false, "Anonymous", null, DisplayName = "Disabled, no auth headers -> 401 Unauthorized")] + [DataRow(true, "Authenticated", null, DisplayName = "Enabled, no auth headers -> clientRoleHeader set to (authenticated)")] + [DataRow(true, "author", "author", DisplayName = "Enabled, honor the clientRoleHeader (author)")] + [DataRow(true, "Anonymous", "Anonymous", DisplayName = "Enabled, honor the clientRoleHeader (anonymous)")] + public async Task AuthenticateDevModeRequests_FeatureFlagTests( + bool authenticateDevModeRequests, string expectedClientRoleHeader, string clientRoleHeader) { @@ -268,20 +370,22 @@ await SendRequestAndGetHttpContextState( token: null, easyAuthType: EasyAuthType.StaticWebApps, clientRoleHeader: clientRoleHeader, - treatRequestAsAuthenticated: treatDevModeRequestAsAuthenticated); + treatRequestAsAuthenticated: authenticateDevModeRequests); + // Validate state of HttpContext after being processed by authentication middleware. Assert.IsNotNull(postMiddlewareContext.User.Identity); Assert.AreEqual(expected: (int)HttpStatusCode.OK, actual: postMiddlewareContext.Response.StatusCode); - Assert.AreEqual(expected: expectedClientRoleHeader, - actual: postMiddlewareContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER].ToString()); - // Validates that AuthenticationMiddleware adds the clientRoleHeader as a role claim - // ONLY when the DevModeAuthNFlag is set. + string resolvedRoleHeader = postMiddlewareContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER].ToString(); + if (!string.IsNullOrWhiteSpace(resolvedRoleHeader)) + { + Assert.AreEqual(expected: expectedClientRoleHeader, actual: resolvedRoleHeader); + } + + // Validates that AuthenticationMiddleware adds the clientRoleHeader as a role claim ONLY when the DevModeAuthNFlag is set. if (clientRoleHeader is not null) { - Assert.AreEqual( - expected: treatDevModeRequestAsAuthenticated, - actual: postMiddlewareContext.User.IsInRole(clientRoleHeader)); + Assert.AreEqual(expected: authenticateDevModeRequests, actual: postMiddlewareContext.User.IsInRole(clientRoleHeader)); } } #endregion diff --git a/src/Service/AuthenticationHelpers/AppServiceAuthentication.cs b/src/Service/AuthenticationHelpers/AppServiceAuthentication.cs index 96fc82df34..9794a2b537 100644 --- a/src/Service/AuthenticationHelpers/AppServiceAuthentication.cs +++ b/src/Service/AuthenticationHelpers/AppServiceAuthentication.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Security.Claims; using System.Text; using System.Text.Json; @@ -20,29 +21,42 @@ public static class AppServiceAuthentication /// Representation of authenticated user principal Http header /// injected by EasyAuth /// - public struct AppServiceClientPrincipal + public class AppServiceClientPrincipal { - public string Auth_typ { get; set; } - public string Name_typ { get; set; } - public string Role_typ { get; set; } - public IEnumerable Claims { get; set; } + /// + /// The type of authentication used, unauthenticated request when null. + /// + public string Auth_typ { get; set; } = null!; + /// + /// The Claim.Type used when obtaining the value of . + /// + /// + public string? Name_typ { get; set; } + /// + /// The Claim.Type used when performing logic for . + /// + /// + public string? Role_typ { get; set; } + /// + /// Collection of claims optionally present. + /// + public IEnumerable? Claims { get; set; } } /// /// Representation of authenticated user principal claims /// injected by EasyAuth /// - public struct AppServiceClaim + public class AppServiceClaim { - public string Typ { get; set; } - public string Val { get; set; } + public string? Typ { get; set; } + public string? Val { get; set; } } /// - /// Create ClaimsIdentity object from EasyAuth - /// injected x-ms-client-principal injected header, - /// the value is a base64 encoded custom JWT injected by EasyAuth - /// as a result of validating a bearer token. + /// Create ClaimsIdentity object from EasyAuth injected x-ms-client-principal injected header, + /// the value is a base64 encoded custom JWT injected by EasyAuth as a result of validating a bearer token. + /// If present, copies all AppService token claims to .NET ClaimsIdentity object. /// /// Request's Http Context /// @@ -61,19 +75,29 @@ public struct AppServiceClaim string encodedPrincipalData = header[0]; byte[] decodedPrincpalData = Convert.FromBase64String(encodedPrincipalData); string json = Encoding.UTF8.GetString(decodedPrincpalData); - AppServiceClientPrincipal principal = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + AppServiceClientPrincipal? principal = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - identity = new(principal.Auth_typ, principal.Name_typ, principal.Role_typ); - - if (principal.Claims != null) + if (!string.IsNullOrEmpty(principal?.Auth_typ)) { - foreach (AppServiceClaim claim in principal.Claims) + // When Name_typ and Role_type are null, ClaimsIdentity contructor uses default values. + // Auth_typ must not be null or empty for ClaimsIdentity.IsAuthenticated() to be true. + // Whitespace is not a requirement per: https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimsidentity.isauthenticated?view=net-6.0#remarks + identity = new(principal.Auth_typ, principal.Name_typ, principal.Role_typ); + + if (principal.Claims is not null && principal.Claims.Any()) { - identity.AddClaim(new Claim(type: claim.Typ, value: claim.Val)); + identity.AddClaims(principal.Claims + .Where(claim => claim.Typ is not null && claim.Val is not null) + .Select(claim => new Claim(type: claim.Typ!, value: claim.Val!)) + ); } } } - catch (Exception error) + catch (Exception error) when ( + error is JsonException || + error is ArgumentNullException || + error is NotSupportedException || + error is InvalidOperationException) { // Logging the parsing failure exception to the console, but not rethrowing // nor creating a DataApiBuilder exception because the authentication handler diff --git a/src/Service/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs b/src/Service/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs index 695e67643d..09e7c06268 100644 --- a/src/Service/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs +++ b/src/Service/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs @@ -59,10 +59,9 @@ public async Task InvokeAsync(HttpContext httpContext) // Write challenge response metadata (HTTP 401 Unauthorized response code // and www-authenticate headers) to the HTTP Context via JwtBearerHandler code // https://github.com/dotnet/aspnetcore/blob/3fe12b935c03138f76364dc877a7e069e254b5b2/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs#L217 - if (authNResult.Failure is not null) + if (authNResult.Failure is not null && !_runtimeConfigurationProvider.IsAuthenticatedDevModeRequest()) { await httpContext.ChallengeAsync(); - return; } diff --git a/src/Service/AuthenticationHelpers/EasyAuthAuthenticationDefaults.cs b/src/Service/AuthenticationHelpers/EasyAuthAuthenticationDefaults.cs index 362dca1880..acf036e248 100644 --- a/src/Service/AuthenticationHelpers/EasyAuthAuthenticationDefaults.cs +++ b/src/Service/AuthenticationHelpers/EasyAuthAuthenticationDefaults.cs @@ -9,5 +9,7 @@ public static class EasyAuthAuthenticationDefaults /// The default value used for StaticWebAppAuthenticationOptions.AuthenticationScheme. /// public const string AUTHENTICATIONSCHEME = "EasyAuthAuthentication"; + + public const string INVALID_PAYLOAD_ERROR = "Invalid EasyAuth Payload."; } } diff --git a/src/Service/AuthenticationHelpers/EasyAuthAuthenticationHandler.cs b/src/Service/AuthenticationHelpers/EasyAuthAuthenticationHandler.cs index fa9aaab361..9783934eb8 100644 --- a/src/Service/AuthenticationHelpers/EasyAuthAuthenticationHandler.cs +++ b/src/Service/AuthenticationHelpers/EasyAuthAuthenticationHandler.cs @@ -38,12 +38,12 @@ ISystemClock clock } /// - /// Gets any authentication data for a request. When an EasyAuth header is present, - /// parses the header and authenticates the user within a ClaimsPrincipal object. + /// Attempts processing of a request's authentication metadata. + /// When an EasyAuth header is present, parses the header and authenticates the user within a ClaimsPrincipal object. /// The ClaimsPrincipal is a security principal usable by middleware to identify the /// authenticated user. /// - /// An authentication result to ASP.NET Core library authentication mechanisms + /// AuthenticatedResult (Fail, NoResult, Success). protected override Task HandleAuthenticateAsync() { if (Context.Request.Headers[AuthenticationConfig.CLIENT_PRINCIPAL_HEADER].Count > 0) @@ -55,13 +55,19 @@ protected override Task HandleAuthenticateAsync() _ => null }; - if (identity is null || HasOnlyAnonymousRole(identity.Claims)) + // If identity is null when the X-MS-CLIENT-PRINCIPAL header is present, + // the header payload failed to parse -> Authentication Failure. + if (identity is null) { - // Either the token is invalid, Or the role is only anonymous, - // we don't terminate the pipeline since the request is - // always at least in the anonymous role. - // It means that anything that is exposed anonymously will still be visible. - // The role assigned to X-MS-API-ROLE will be anonymous. + return Task.FromResult(AuthenticateResult.Fail(failureMessage: EasyAuthAuthenticationDefaults.INVALID_PAYLOAD_ERROR)); + } + + if (HasOnlyAnonymousRole(identity.Claims)) + { + // When EasyAuth is properly configured, do not terminate the request pipeline + // since a request is always at least in the anonymous role. + // This result signals that authentication did not fail, though the request + // should be evaluated as unauthenticated. return Task.FromResult(AuthenticateResult.NoResult()); } @@ -77,11 +83,8 @@ protected override Task HandleAuthenticateAsync() } } - // Return no result when no EasyAuth header is present, - // because a request is always in anonymous role in EasyAuth - // This scenario is not possible when front loaded with EasyAuth - // since the X-MS-CLIENT-PRINCIPAL header will always be present in that case. - // This is applicable when engine is being tested without front loading with EasyAuth. + // The EasyAuth (X-MS-CLIENT-PRINCIPAL) header will only be present in a properly configured environment + // for authenticated requests and not anonymous requests. return Task.FromResult(AuthenticateResult.NoResult()); } @@ -90,7 +93,7 @@ protected override Task HandleAuthenticateAsync() /// /// /// - private static bool HasOnlyAnonymousRole(IEnumerable claims) + public static bool HasOnlyAnonymousRole(IEnumerable claims) { bool isUserAnonymousOnly = false; foreach (Claim claim in claims)