From 834670b3f2d671afb56ca8b742cee4223728d567 Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Mon, 28 Nov 2022 18:15:42 -0800 Subject: [PATCH] Updated and added unit tests to match EasyAuth functionality for AppService (Claims Included) and SWA (no claims, only userId and userDetails are present). --- src/Service.Tests/AuthTestHelper.cs | 29 ++++--- .../EasyAuthAuthenticationUnitTests.cs | 85 +++++++++++++------ .../StaticWebAppsAuthentication.cs | 36 +++----- 3 files changed, 91 insertions(+), 59 deletions(-) diff --git a/src/Service.Tests/AuthTestHelper.cs b/src/Service.Tests/AuthTestHelper.cs index 80d7aa3a70..8addcfa2bc 100644 --- a/src/Service.Tests/AuthTestHelper.cs +++ b/src/Service.Tests/AuthTestHelper.cs @@ -14,7 +14,7 @@ internal static class AuthTestHelper /// Creates a mocked EasyAuth token, namely, the value of the header injected by EasyAuth. /// /// A Base64 encoded string of a serialized EasyAuthClientPrincipal object - public static string CreateAppServiceEasyAuthToken() + public static string CreateAppServiceEasyAuthToken(IEnumerable additionalClaims = null) { AppServiceClaim emailClaim = new() { @@ -34,15 +34,22 @@ public static string CreateAppServiceEasyAuthToken() Typ = ClaimTypes.Role }; - List claims = new(); - claims.Add(emailClaim); - claims.Add(roleClaimAnonymous); - claims.Add(roleClaimAuthenticated); + HashSet claims = new() + { + emailClaim, + roleClaimAnonymous, + roleClaimAuthenticated + }; + + if (additionalClaims != null) + { + claims.UnionWith(additionalClaims); + } AppServiceClientPrincipal token = new() { Auth_typ = "aad", - Name_typ = "Apple Banana", + Name_typ = "name", Claims = claims, Role_typ = ClaimTypes.Role }; @@ -61,7 +68,8 @@ public static string CreateAppServiceEasyAuthToken() public static string CreateStaticWebAppsEasyAuthToken( bool addAuthenticated = true, string specificRole = null, - IEnumerable claims = null) + string userId = null, + string userDetails = null) { // The anonymous role is present in all requests sent to Static Web Apps or AppService endpoints. List roles = new() @@ -93,9 +101,10 @@ public static string CreateStaticWebAppsEasyAuthToken( StaticWebAppsClientPrincipal token = new() { - IdentityProvider = "github", - UserRoles = roles, - Claims = claims + UserId = userId, + UserDetails = userDetails, + IdentityProvider = "aad", + UserRoles = roles }; string serializedToken = JsonSerializer.Serialize(value: token); diff --git a/src/Service.Tests/Authentication/EasyAuthAuthenticationUnitTests.cs b/src/Service.Tests/Authentication/EasyAuthAuthenticationUnitTests.cs index c55f808dce..0d80f3de96 100644 --- a/src/Service.Tests/Authentication/EasyAuthAuthenticationUnitTests.cs +++ b/src/Service.Tests/Authentication/EasyAuthAuthenticationUnitTests.cs @@ -4,6 +4,7 @@ using System.Net; using System.Threading.Tasks; using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Service.AuthenticationHelpers; using Azure.DataApiBuilder.Service.Authorization; using Azure.DataApiBuilder.Service.Tests.Authentication.Helpers; using Microsoft.AspNetCore.Http; @@ -11,7 +12,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Primitives; using Microsoft.VisualStudio.TestTools.UnitTesting; -using static Azure.DataApiBuilder.Service.AuthenticationHelpers.StaticWebAppsAuthentication; +using static Azure.DataApiBuilder.Service.AuthenticationHelpers.AppServiceAuthentication; namespace Azure.DataApiBuilder.Service.Tests.Authentication { @@ -79,16 +80,14 @@ public async Task TestValidStaticWebAppsEasyAuthToken(bool sendAuthorizationHead } /// - /// Ensures SWA token payload claims are processed by - /// validating that those claims are present + /// Ensures AppService EasyAuth payload claims are processed by validating that those claims are present /// on the authenticated .NET ClaimsPrincipal object. - /// Demonstrates using the immutable claim values tid and oid - /// as a combined key for uniquely identifying the API's data - /// and determining whether a user should be granted access to that data. + /// Demonstrates using the immutable claim values tid and oid as a combined key for uniquely identifying + /// the API's data and determining whether a user should be granted access to that data. /// /// [TestMethod] - public async Task TestStaticWebAppsEasyAuthTokenClaims() + public async Task TestAppServiceEasyAuthTokenClaims() { string objectIdClaimType = "oid"; string objectId = "f35eaa76-b8e6-4c7c-99a2-5aeeeee9ba58"; @@ -96,18 +95,17 @@ public async Task TestStaticWebAppsEasyAuthTokenClaims() string tenantIdClaimType = "tid"; string tenantId = "8f902aef-2c06-42c9-a3d0-bc31f04a3dca"; - List payloadClaims = new(); - payloadClaims.Add(new SWAPrincipalClaim() { Typ = objectIdClaimType, Val = objectId }); - payloadClaims.Add(new SWAPrincipalClaim() { Typ = tenantIdClaimType, Val = tenantId }); + List payloadClaims = new() + { + new AppServiceClaim() { Typ = objectIdClaimType, Val = objectId }, + new AppServiceClaim() { Typ = tenantIdClaimType, Val = tenantId } + }; - string generatedToken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken( - addAuthenticated: true, - claims: payloadClaims - ); + string generatedToken = AuthTestHelper.CreateAppServiceEasyAuthToken(additionalClaims: payloadClaims); HttpContext postMiddlewareContext = await SendRequestAndGetHttpContextState( generatedToken, - EasyAuthType.StaticWebApps); + EasyAuthType.AppService); Assert.IsNotNull(postMiddlewareContext.User.Identity); Assert.IsTrue(postMiddlewareContext.User.Identity.IsAuthenticated); @@ -117,11 +115,9 @@ public async Task TestStaticWebAppsEasyAuthTokenClaims() } /// - /// Validates that null claim type and/or null claim value - /// are not processed claims on the .NET ClaimsPrincipal object, - /// due lack of null claim type/ claim value support on the ClaimsPrincipal. - /// Validates that empty string for claim type and/or value - /// is processed successfully. + /// Validates that null claim type and/or null claim value are not processed claims on the + /// .NET ClaimsPrincipal object, due lack of null claim type/ claim value support on the ClaimsPrincipal. + /// Validates that empty string for claim type and/or value is processed successfully. /// /// string representation of claim type /// string representation of claim value @@ -135,19 +131,20 @@ public async Task TestStaticWebAppsEasyAuthTokenClaims() [DataRow("tid", "", true, DisplayName = "Claim value empty string - will process")] [DataRow("", "", true, DisplayName = "Claim type/value empty string - will process")] - public async Task TestStaticWebAppsEasyAuth_IncompleteTokenClaims(string? claimType, string? claimValue, bool expectProcessedClaim) + public async Task TestAppServiceEasyAuth_IncompleteTokenClaims(string? claimType, string? claimValue, bool expectProcessedClaim) { - List payloadClaims = new(); - payloadClaims.Add(new SWAPrincipalClaim() { Typ = claimType, Val = claimValue }); + List payloadClaims = new() + { + new AppServiceClaim() { Typ = claimType, Val = claimValue } + }; - string generatedToken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken( - addAuthenticated: true, - claims: payloadClaims + string generatedToken = AuthTestHelper.CreateAppServiceEasyAuthToken( + additionalClaims: payloadClaims ); HttpContext postMiddlewareContext = await SendRequestAndGetHttpContextState( generatedToken, - EasyAuthType.StaticWebApps); + EasyAuthType.AppService); Assert.IsNotNull(postMiddlewareContext.User.Identity); Assert.IsTrue(postMiddlewareContext.User.Identity.IsAuthenticated); @@ -161,6 +158,40 @@ public async Task TestStaticWebAppsEasyAuth_IncompleteTokenClaims(string? claimT Assert.AreEqual(expected: (int)HttpStatusCode.OK, actual: postMiddlewareContext.Response.StatusCode); } + /// + /// Tests that a populated userId and/or userDetails property on the SWA Authenticated user payload + /// results in the created ClaimsIdentity object having a matching claim for the populated property. + /// + /// SWA userId property value. + /// SWA userDetails property value. + /// Whether claim matching property should be present on ClaimsIdentity object. + [DataTestMethod] + [DataRow("1337", "UserDetailsString", true, DisplayName = "UserId and UserDetails Claims Match SWA User Payload")] + [DataRow("", "", false, DisplayName = "Empty properties in SWA User Payload -> No Matching Claims")] + [DataRow(null, null, false, DisplayName = "Null properties in SWA User Payload -> No Matching Claims")] + public async Task TestStaticWebAppsEasyAuthToken_PropertiesToClaims(string userId, string userDetails, bool expectClaim) + { + string generatedToken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken(addAuthenticated: true, userId: userId, userDetails: userDetails); + HttpContext postMiddlewareContext = await SendRequestAndGetHttpContextState(generatedToken, EasyAuthType.StaticWebApps); + + Assert.IsNotNull(postMiddlewareContext.User.Identity); + Assert.IsTrue(postMiddlewareContext.User.Identity.IsAuthenticated); + Assert.AreEqual(expected: (int)HttpStatusCode.OK, actual: postMiddlewareContext.Response.StatusCode); + + // If userId and/or userDetails are null in the EasyAuth payload, a claim will NOT be added to the ClaimsIdentity object + // for the null/empty/whitespace property. + if (expectClaim) + { + Assert.IsTrue(postMiddlewareContext.User.HasClaim(type: StaticWebAppsAuthentication.USER_ID_CLAIM, value: userId)); + Assert.IsTrue(postMiddlewareContext.User.HasClaim(type: StaticWebAppsAuthentication.USER_DETAILS_CLAIM, value: userDetails)); + } + else + { + Assert.IsFalse(postMiddlewareContext.User.HasClaim(type: StaticWebAppsAuthentication.USER_ID_CLAIM, value: string.Empty)); + Assert.IsFalse(postMiddlewareContext.User.HasClaim(type: StaticWebAppsAuthentication.USER_DETAILS_CLAIM, value: string.Empty)); + } + } + /// /// When the user request is a valid token but only has an anonymous role, /// we still return OK. We assign the client role header to be anonymous. diff --git a/src/Service/AuthenticationHelpers/StaticWebAppsAuthentication.cs b/src/Service/AuthenticationHelpers/StaticWebAppsAuthentication.cs index 0c1a991173..a1b813d2e5 100644 --- a/src/Service/AuthenticationHelpers/StaticWebAppsAuthentication.cs +++ b/src/Service/AuthenticationHelpers/StaticWebAppsAuthentication.cs @@ -17,6 +17,9 @@ namespace Azure.DataApiBuilder.Service.AuthenticationHelpers /// public class StaticWebAppsAuthentication { + public const string USER_ID_CLAIM = "userId"; + public const string USER_DETAILS_CLAIM = "userDetails"; + /// /// Link for reference of how StaticWebAppsClientPrincipal is defined /// https://docs.microsoft.com/azure/static-web-apps/user-information?tabs=csharp#client-principal-data @@ -27,16 +30,6 @@ public class StaticWebAppsClientPrincipal public string? UserId { get; set; } public string? UserDetails { get; set; } public IEnumerable? UserRoles { get; set; } - public IEnumerable? Claims { get; set; } - } - - /// - /// Representation of a user claim in a SWA token payload. - /// - public class SWAPrincipalClaim - { - public string? Typ { get; set; } - public string? Val { get; set; } } /// @@ -67,23 +60,22 @@ public class SWAPrincipalClaim return identity; } - identity = new(principal.IdentityProvider, nameType: ClaimTypes.Name, roleType: AuthenticationConfig.ROLE_CLAIM_TYPE); - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, principal.UserId ?? string.Empty)); - identity.AddClaim(new Claim(ClaimTypes.Name, principal.UserDetails ?? string.Empty)); + identity = new(authenticationType: principal.IdentityProvider, nameType: USER_ID_CLAIM, roleType: AuthenticationConfig.ROLE_CLAIM_TYPE); - // output identity.Claims - // [0] { Type = "role", Value = "roleName" } - identity.AddClaims(principal.UserRoles.Select(roleName => new Claim(AuthenticationConfig.ROLE_CLAIM_TYPE, roleName))); + if (!string.IsNullOrWhiteSpace(principal.UserId)) + { + identity.AddClaim(new Claim(USER_ID_CLAIM, principal.UserId)); + } - // Copy all SWA token claims to .NET ClaimsIdentity object. - if (principal.Claims is not null && principal.Claims.Any()) + if (!string.IsNullOrWhiteSpace(principal.UserDetails)) { - 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!)) - ); + identity.AddClaim(new Claim(USER_DETAILS_CLAIM, principal.UserDetails)); } + // output identity.Claims + // [0] { Type = "roles", Value = "roleName" } + identity.AddClaims(principal.UserRoles.Select(roleName => new Claim(AuthenticationConfig.ROLE_CLAIM_TYPE, roleName))); + return identity; } catch (Exception error) when (