Skip to content
18 changes: 13 additions & 5 deletions DataGateway.Config/Authentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,19 @@ namespace Azure.DataGateway.Config
/// <summary>
/// Authentication configuration.
/// </summary>
/// <param name="Provider">Identity Provider. Default is EasyAuth.
/// <param name="Provider">Identity Provider. Default is StaticWebApps.
/// With EasyAuth, no Audience or Issuer are expected.
/// </param>
/// <param name="Jwt">Settings enabling validation of the received JWT token.
/// Required only when Provider is other than EasyAuth.</param>
public record AuthenticationConfig(
string Provider = AuthenticationConfig.EASYAUTH_PROVIDER_NAME,
string Provider,
Jwt? Jwt = null)
{
public const string EASYAUTH_PROVIDER_NAME = "EasyAuth";

public const string CLIENT_PRINCIPAL_HEADER = "X-MS-CLIENT-PRINCIPAL";
public bool IsEasyAuthAuthenticationProvider()
{
return Provider.Equals(EASYAUTH_PROVIDER_NAME);
return Enum.GetNames(typeof(EasyAuthType)).Any(x => x.Equals(Provider, StringComparison.OrdinalIgnoreCase));
}
}

Expand All @@ -26,4 +25,13 @@ public bool IsEasyAuthAuthenticationProvider()
/// <param name="Audience"></param>
/// <param name="Issuer"></param>
public record Jwt(string Audience, string Issuer);

Comment thread
tarazou9 marked this conversation as resolved.
/// <summary>
/// Different modes in which the runtime can run.
/// </summary>
public enum EasyAuthType
{
StaticWebApps,
AppService
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Azure.DataGateway.Config;
using Azure.DataGateway.Service.AuthenticationHelpers;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
Expand All @@ -17,7 +18,8 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static Azure.DataGateway.Service.AuthenticationHelpers.EasyAuthAuthentication;
using static Azure.DataGateway.Service.AuthenticationHelpers.AppServiceAuthentication;
using static Azure.DataGateway.Service.AuthenticationHelpers.StaticWebAppsAuthentication;

namespace Azure.DataGateway.Service.Tests.Authentication
{
Expand All @@ -30,19 +32,38 @@ public class EasyAuthAuthenticationUnitTests
{
#region Positive Tests
/// <summary>
/// Ensures a valid EasyAuth header/value does NOT result in HTTP 401 Unauthorized response.
/// Ensures a valid AppService EasyAuth header/value does NOT result in HTTP 401 Unauthorized 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.
/// </summary>
[DataTestMethod]
[DataRow(false, DisplayName = "Valid EasyAuth header only")]
[DataRow(true, DisplayName = "Valid EasyAuth header and authorization header")]
[DataRow(false, DisplayName = "Valid AppService EasyAuth header only")]
[DataRow(true, DisplayName = "Valid AppService EasyAuth header and authorization header")]
[TestMethod]
public async Task TestValidEasyAuthToken(bool sendAuthorizationHeader)
public async Task TestValidAppServiceEasyAuthToken(bool sendAuthorizationHeader)
{
string generatedToken = CreateEasyAuthToken();
HttpContext postMiddlewareContext = await SendRequestAndGetHttpContextState(generatedToken);
string generatedToken = CreateAppServiceEasyAuthToken();
HttpContext postMiddlewareContext = await SendRequestAndGetHttpContextState(generatedToken, EasyAuthType.AppService);
Comment thread
tarazou9 marked this conversation as resolved.
Assert.IsNotNull(postMiddlewareContext.User.Identity);
Assert.IsTrue(postMiddlewareContext.User.Identity.IsAuthenticated);
Assert.AreEqual(expected: (int)HttpStatusCode.OK, actual: postMiddlewareContext.Response.StatusCode);
}

/// <summary>
/// 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.
/// When an authorization header is sent, it contains an invalid value, if the runtime returns an error
/// then there is improper JWT validation occurring.
/// </summary>
[DataTestMethod]
[DataRow(false, DisplayName = "Valid StaticWebApps EasyAuth header only")]
[DataRow(true, DisplayName = "Valid StaticWebApps EasyAuth header and authorization header")]
[TestMethod]
public async Task TestValidStaticWebAppsEasyAuthToken(bool sendAuthorizationHeader)
{
string generatedToken = CreateStaticWebAppsEasyAuthToken();
HttpContext postMiddlewareContext = await SendRequestAndGetHttpContextState(generatedToken, EasyAuthType.StaticWebApps);
Comment thread
tarazou9 marked this conversation as resolved.
Assert.IsNotNull(postMiddlewareContext.User.Identity);
Assert.IsTrue(postMiddlewareContext.User.Identity.IsAuthenticated);
Assert.AreEqual(expected: (int)HttpStatusCode.OK, actual: postMiddlewareContext.Response.StatusCode);
Expand All @@ -68,7 +89,7 @@ public async Task TestValidEasyAuthToken(bool sendAuthorizationHeader)
[TestMethod]
public async Task TestInvalidEasyAuthToken(string token, bool sendAuthorizationHeader = false)
{
HttpContext postMiddlewareContext = await SendRequestAndGetHttpContextState(token, sendAuthorizationHeader);
HttpContext postMiddlewareContext = await SendRequestAndGetHttpContextState(token, EasyAuthType.StaticWebApps, sendAuthorizationHeader);
Assert.IsNotNull(postMiddlewareContext.User.Identity);
Assert.IsFalse(postMiddlewareContext.User.Identity.IsAuthenticated);
Assert.AreEqual(expected: (int)HttpStatusCode.Unauthorized, actual: postMiddlewareContext.Response.StatusCode);
Expand All @@ -80,7 +101,7 @@ public async Task TestInvalidEasyAuthToken(string token, bool sendAuthorizationH
/// Configures test server with bare minimum middleware
/// </summary>
/// <returns>IHost</returns>
private static async Task<IHost> CreateWebHostEasyAuth()
private static async Task<IHost> CreateWebHostEasyAuth(EasyAuthType easyAuthType)
{
return await new HostBuilder()
.ConfigureWebHost(webBuilder =>
Expand All @@ -90,7 +111,8 @@ private static async Task<IHost> CreateWebHostEasyAuth()
.ConfigureServices(services =>
{
services.AddAuthentication(defaultScheme: EasyAuthAuthenticationDefaults.AUTHENTICATIONSCHEME)
.AddEasyAuthAuthentication();
.AddEasyAuthAuthentication(easyAuthType);

services.AddAuthorization();
})
.ConfigureLogging(o =>
Expand Down Expand Up @@ -125,17 +147,17 @@ private static async Task<IHost> CreateWebHostEasyAuth()
/// <param name="token">The EasyAuth header value(base64 encoded token) to test against the TestServer</param>
/// <param name="sendAuthorizationHeader">Whether to add authorization header to header dictionary</param>
/// <returns></returns>
private static async Task<HttpContext> SendRequestAndGetHttpContextState(string? token, bool sendAuthorizationHeader = false)
private static async Task<HttpContext> SendRequestAndGetHttpContextState(string? token, EasyAuthType easyAuthType, bool sendAuthorizationHeader = false)
{
using IHost host = await CreateWebHostEasyAuth();
using IHost host = await CreateWebHostEasyAuth(easyAuthType);
TestServer server = host.GetTestServer();

return await server.SendAsync(context =>
{
if (token is not null)
{
StringValues headerValue = new(new string[] { $"{token}" });
KeyValuePair<string, StringValues> easyAuthHeader = new(EasyAuthAuthentication.EASYAUTHHEADER, headerValue);
KeyValuePair<string, StringValues> easyAuthHeader = new(AuthenticationConfig.CLIENT_PRINCIPAL_HEADER, headerValue);
context.Request.Headers.Add(easyAuthHeader);
}

Expand All @@ -153,25 +175,25 @@ private static async Task<HttpContext> SendRequestAndGetHttpContextState(string?
/// Creates a mocked EasyAuth token, namely, the value of the header injected by EasyAuth.
/// </summary>
/// <returns>A Base64 encoded string of a serialized EasyAuthClientPrincipal object</returns>
private static string CreateEasyAuthToken()
private static string CreateAppServiceEasyAuthToken()
{
EasyAuthClaim emailClaim = new()
AppServiceClaim emailClaim = new()
{
Val = "apple@contoso.com",
Typ = ClaimTypes.Upn
};

EasyAuthClaim roleClaim = new()
AppServiceClaim roleClaim = new()
{
Val = "Anonymous",
Typ = ClaimTypes.Role
};

List<EasyAuthClaim> claims = new();
List<AppServiceClaim> claims = new();
claims.Add(emailClaim);
claims.Add(roleClaim);

EasyAuthClientPrincipal token = new()
AppServiceClientPrincipal token = new()
{
Auth_typ = "aad",
Name_typ = "Apple Banana",
Expand All @@ -182,6 +204,26 @@ private static string CreateEasyAuthToken()
string serializedToken = JsonSerializer.Serialize(value: token);
return Convert.ToBase64String(Encoding.UTF8.GetBytes(serializedToken));
}

/// <summary>
/// Creates a mocked EasyAuth token, namely, the value of the header injected by EasyAuth.
/// </summary>
/// <returns>A Base64 encoded string of a serialized EasyAuthClientPrincipal object</returns>
private static string CreateStaticWebAppsEasyAuthToken()
{
List<string> roles = new();
roles.Add("anonymous");
roles.Add("authenticated");

StaticWebAppsClientPrincipal token = new()
{
IdentityProvider = "github",
UserRoles = roles
};

string serializedToken = JsonSerializer.Serialize(value: token);
return Convert.ToBase64String(Encoding.UTF8.GetBytes(serializedToken));
}
#endregion
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public static AuthorizationResolver InitAuthorizationResolver(RuntimeConfig runt
Mock<ISqlMetadataProvider> metadataProvider = new();
TableDefinition sampleTable = CreateSampleTable();
metadataProvider.Setup(x => x.GetTableDefinition(TEST_ENTITY)).Returns(sampleTable);
metadataProvider.Setup(x => x.GetDatabaseType()).Returns(DatabaseType.mssql);

string outParam;
Dictionary<string, Dictionary<string, string>> _exposedNameToBackingColumnMapping = CreateColumnMappingTable();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class AuthenticationConfigValidatorUnitTests
public void ValidateEasyAuthConfig()
{
RuntimeConfig config =
CreateRuntimeConfigWithAuthN(new AuthenticationConfig());
CreateRuntimeConfigWithAuthN(new AuthenticationConfig(EasyAuthType.StaticWebApps.ToString()));

RuntimeConfigValidator configValidator = GetMockConfigValidator(ref config);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,35 @@
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using Azure.DataGateway.Config;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;

namespace Azure.DataGateway.Service.AuthenticationHelpers
{
/// <summary>
/// Helper class which parses EasyAuth's injected headers into a ClaimsIdentity object.
/// This class provides helper methods for StaticWebApp's Authentication feature: EasyAuth.
/// This class provides helper methods for AppService's Authentication feature: EasyAuth.
/// </summary>
public static class EasyAuthAuthentication
public static class AppServiceAuthentication
{
public const string EASYAUTHHEADER = "X-MS-CLIENT-PRINCIPAL";
/// <summary>
/// Representation of authenticated user principal Http header
/// injected by EasyAuth
/// </summary>
public struct EasyAuthClientPrincipal
public struct AppServiceClientPrincipal
{
public string Auth_typ { get; set; }
Comment thread
tarazou9 marked this conversation as resolved.
public string Name_typ { get; set; }
public string Role_typ { get; set; }
public IEnumerable<EasyAuthClaim> Claims { get; set; }
public IEnumerable<AppServiceClaim> Claims { get; set; }
}

/// <summary>
/// Representation of authenticated user principal claims
/// injected by EasyAuth
/// </summary>
public struct EasyAuthClaim
public struct AppServiceClaim
{
public string Typ { get; set; }
public string Val { get; set; }
Expand All @@ -53,20 +53,20 @@ public struct EasyAuthClaim
{
ClaimsIdentity? identity = null;

if (context.Request.Headers.TryGetValue(EasyAuthAuthentication.EASYAUTHHEADER, out StringValues header))
if (context.Request.Headers.TryGetValue(AuthenticationConfig.CLIENT_PRINCIPAL_HEADER, out StringValues header))
{
try
{
string encodedPrincipalData = header[0];
byte[] decodedPrincpalData = Convert.FromBase64String(encodedPrincipalData);
string json = Encoding.UTF8.GetString(decodedPrincpalData);
EasyAuthClientPrincipal principal = JsonSerializer.Deserialize<EasyAuthClientPrincipal>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
AppServiceClientPrincipal principal = JsonSerializer.Deserialize<AppServiceClientPrincipal>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

identity = new(principal.Auth_typ, principal.Name_typ, principal.Role_typ);

if (principal.Claims != null)
{
foreach (EasyAuthClaim claim in principal.Claims)
foreach (AppServiceClaim claim in principal.Claims)
{
identity.AddClaim(new Claim(type: claim.Typ, value: claim.Val));
}
Expand All @@ -77,7 +77,7 @@ public struct EasyAuthClaim
// Logging the parsing failure exception to the console, but not rethrowing
// nor creating a DataGateway exception because the authentication handler
// will create and send a 401 unauthorized response to the client.
Console.Error.WriteLine("Failure processing the EasyAuth header.");
Console.Error.WriteLine("Failure processing the AppService EasyAuth header.");
Console.Error.WriteLine(error.Message);
Console.Error.WriteLine(error.StackTrace);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Azure.DataGateway.Config;
using Microsoft.AspNetCore.Authentication;

namespace Azure.DataGateway.Service.AuthenticationHelpers
Expand All @@ -13,9 +14,10 @@ public static class EasyAuthAuthenticationBuilderExtensions
/// Add authentication with Static Web Apps.
/// </summary>
/// <param name="builder">Authentication builder.</param>
/// <param name="easyAuthAuthenticationProvider">EasyAuth provider type. StaticWebApps or AppService</param>
/// <returns>The builder, to chain commands.</returns>
public static AuthenticationBuilder AddEasyAuthAuthentication(
this AuthenticationBuilder builder)
this AuthenticationBuilder builder, EasyAuthType easyAuthAuthenticationProvider)
{
if (builder is null)
{
Expand All @@ -25,8 +27,17 @@ public static AuthenticationBuilder AddEasyAuthAuthentication(
builder.AddScheme<EasyAuthAuthenticationOptions, EasyAuthAuthenticationHandler>(
authenticationScheme: EasyAuthAuthenticationDefaults.AUTHENTICATIONSCHEME,
displayName: EasyAuthAuthenticationDefaults.AUTHENTICATIONSCHEME,
options => { });

options =>
{
if (easyAuthAuthenticationProvider is EasyAuthType.StaticWebApps)
{
options.EasyAuthProvider = EasyAuthType.StaticWebApps;
}
else if (easyAuthAuthenticationProvider is EasyAuthType.AppService)
Comment thread
tarazou9 marked this conversation as resolved.
{
options.EasyAuthProvider = EasyAuthType.AppService;
}
});
return builder;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Azure.DataGateway.Config;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
Expand All @@ -14,18 +15,14 @@ namespace Azure.DataGateway.Service.AuthenticationHelpers
/// and utilizes the base class default handler for
/// - AuthenticateAsync: Authenticates the current request.
/// - Forbid Async: Creates 403 HTTP Response.
/// Usage modelled from Microsoft.Identity.Web.
/// Ref: https://github.com/AzureAD/microsoft-identity-web/blob/master/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationHandler.cs
/// </summary>
public class EasyAuthAuthenticationHandler : AuthenticationHandler<EasyAuthAuthenticationOptions>
{
private const string EASY_AUTH_HEADER = "X-MS-CLIENT-PRINCIPAL";

/// <summary>
/// Constructor for the EasyAuthAuthenticationHandler.
/// Note the parameters are required by the base class.
/// </summary>
/// <param name="options">App service authentication options.</param>
/// <param name="options">Easy Auth authentication options.</param>
/// <param name="logger">Logger factory.</param>
/// <param name="encoder">URL encoder.</param>
/// <param name="clock">System clock.</param>
Expand All @@ -47,16 +44,22 @@ ISystemClock clock
/// <returns>An authentication result to ASP.NET Core library authentication mechanisms</returns>
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (Context.Request.Headers[EASY_AUTH_HEADER].Count > 0)
if (Context.Request.Headers[AuthenticationConfig.CLIENT_PRINCIPAL_HEADER].Count > 0)
{
ClaimsIdentity? identity = EasyAuthAuthentication.Parse(Context);
ClaimsIdentity? identity = Options.EasyAuthProvider switch
{
EasyAuthType.StaticWebApps => StaticWebAppsAuthentication.Parse(Context),
EasyAuthType.AppService => AppServiceAuthentication.Parse(Context),
_ => null
};

if (identity is null)
{
return Task.FromResult(AuthenticateResult.Fail(failureMessage: "Invalid EasyAuth token."));
return Task.FromResult(AuthenticateResult.Fail(failureMessage: $"Invalid {Options.EasyAuthProvider} EasyAuth token."));
}

ClaimsPrincipal? claimsPrincipal = new(identity);

if (claimsPrincipal is not null)
{
// AuthenticationTicket is Asp.Net Core Abstraction of Authentication information
Expand Down
Loading