Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions src/Service.Tests/AuthTestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ internal static class AuthTestHelper
/// <seealso cref="https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimsidentity.roleclaimtype?view=net-6.0"/>
public static string CreateAppServiceEasyAuthToken(
string? nameClaimType = ClaimTypes.Name,
string? roleClaimType = ClaimTypes.Role)
string? roleClaimType = ClaimTypes.Role,
IEnumerable<AppServiceClaim>? additionalClaims = null)
{
AppServiceClaim emailClaim = new()
{
Expand Down Expand Up @@ -65,7 +66,7 @@ public static string CreateAppServiceEasyAuthToken(
Typ = ClaimTypes.Name
};

List<AppServiceClaim> claims = new()
HashSet<AppServiceClaim> claims = new()
{
emailClaim,
roleClaimAnonymous,
Expand All @@ -76,6 +77,11 @@ public static string CreateAppServiceEasyAuthToken(
nameUriClaimType
};

if (additionalClaims != null)
{
claims.UnionWith(additionalClaims);
}

AppServiceClientPrincipal token = new()
{
Auth_typ = "aad",
Expand All @@ -98,7 +104,8 @@ public static string CreateAppServiceEasyAuthToken(
public static string CreateStaticWebAppsEasyAuthToken(
bool addAuthenticated = true,
string? specificRole = null,
IEnumerable<SWAPrincipalClaim>? 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<string> roles = new()
Expand Down Expand Up @@ -130,9 +137,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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
using System.Security.Claims;
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;
using Microsoft.AspNetCore.TestHost;
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
{
Expand Down Expand Up @@ -180,35 +181,32 @@ public async Task TestValidStaticWebAppsEasyAuthToken(bool sendAuthorizationHead
}

/// <summary>
/// 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.
/// </summary>
/// <seealso cref="https://docs.microsoft.com/azure/active-directory/develop/access-tokens#validate-user-permission"/>
[TestMethod]
public async Task TestStaticWebAppsEasyAuthTokenClaims()
public async Task TestAppServiceEasyAuthTokenClaims()
{
string objectIdClaimType = "oid";
string objectId = "f35eaa76-b8e6-4c7c-99a2-5aeeeee9ba58";

string tenantIdClaimType = "tid";
string tenantId = "8f902aef-2c06-42c9-a3d0-bc31f04a3dca";

List<SWAPrincipalClaim> payloadClaims = new();
payloadClaims.Add(new SWAPrincipalClaim() { Typ = objectIdClaimType, Val = objectId });
payloadClaims.Add(new SWAPrincipalClaim() { Typ = tenantIdClaimType, Val = tenantId });
List<AppServiceClaim> 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);
Expand All @@ -218,11 +216,9 @@ public async Task TestStaticWebAppsEasyAuthTokenClaims()
}

/// <summary>
/// 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.
/// </summary>
/// <param name="claimType">string representation of claim type</param>
/// <param name="claimValue">string representation of claim value</param>
Expand All @@ -236,19 +232,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<SWAPrincipalClaim> payloadClaims = new();
payloadClaims.Add(new SWAPrincipalClaim() { Typ = claimType, Val = claimValue });
List<AppServiceClaim> 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);
Expand All @@ -262,6 +259,40 @@ public async Task TestStaticWebAppsEasyAuth_IncompleteTokenClaims(string? claimT
Assert.AreEqual(expected: (int)HttpStatusCode.OK, actual: postMiddlewareContext.Response.StatusCode);
}

/// <summary>
/// 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.
/// </summary>
/// <param name="userId">SWA userId property value.</param>
/// <param name="userDetails">SWA userDetails property value.</param>
/// <param name="expectClaim">Whether claim matching property should be present on ClaimsIdentity object.</param>
[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));
}
}

/// <summary>
/// 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.
Expand Down
36 changes: 14 additions & 22 deletions src/Service/AuthenticationHelpers/StaticWebAppsAuthentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ namespace Azure.DataApiBuilder.Service.AuthenticationHelpers
/// </summary>
public class StaticWebAppsAuthentication
{
public const string USER_ID_CLAIM = "userId";
public const string USER_DETAILS_CLAIM = "userDetails";

/// <summary>
/// Link for reference of how StaticWebAppsClientPrincipal is defined
/// https://docs.microsoft.com/azure/static-web-apps/user-information?tabs=csharp#client-principal-data
Expand All @@ -27,16 +30,6 @@ public class StaticWebAppsClientPrincipal
public string? UserId { get; set; }
public string? UserDetails { get; set; }
public IEnumerable<string>? UserRoles { get; set; }
public IEnumerable<SWAPrincipalClaim>? Claims { get; set; }
}

/// <summary>
/// Representation of a user claim in a SWA token payload.
/// </summary>
public class SWAPrincipalClaim
{
public string? Typ { get; set; }
public string? Val { get; set; }
}

/// <summary>
Expand Down Expand Up @@ -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 (
Expand Down