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)