Skip to content

Commit 5c21dcc

Browse files
authored
Fixes #273 (#288)
Exposed `DurableFunctionsMonitor.DotNetIsolated.GetClaimsPricipal()` as a public method to be used by `DurableFunctionsMonitor.DotNetIsolated.DfmGetEasyAuthConfigFunction()` The main purpose of the change was to get a `ClaimsPrincipal` that was constructed from the `X-MS-CLIENT-PRINCIPAL` header if needed.
1 parent 3d4b373 commit 5c21dcc

File tree

4 files changed

+429
-2
lines changed

4 files changed

+429
-2
lines changed

durablefunctionsmonitor.dotnetisolated.core/Common/Auth.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ private static string TryGetHubNameFromHostJson()
345345
}
346346
}
347347

348-
private static async Task<ClaimsPrincipal> GetClaimsPrincipal(HttpRequestData request, DfmSettings settings)
348+
public static async Task<ClaimsPrincipal> GetClaimsPrincipal(HttpRequestData request, DfmSettings settings)
349349
{
350350
// First trying the request object
351351
var easyAuthPrincipal = new ClaimsPrincipal(request.Identities);

durablefunctionsmonitor.dotnetisolated.core/Functions/EasyAuthConfig.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ public async Task<HttpResponseData> DfmGetEasyAuthConfigFunction(
5353
{
5454
// Assuming it is the server-directed login flow to be used
5555
// and returning just the user name (to speed up login process)
56-
var userNameClaim = req.Identities?.SingleOrDefault()?.FindAll(this.Settings.UserNameClaimName).SingleOrDefault();
56+
var claimsPrincipal = await Auth.GetClaimsPrincipal(req, this.Settings);
57+
var userNameClaim = claimsPrincipal?.Identities?.SingleOrDefault()?.FindAll(this.Settings.UserNameClaimName).SingleOrDefault();
5758
return await req.ReturnJson(new { userName = userNameClaim?.Value });
5859
}
5960

tests/durablefunctionsmonitor.dotnetisolated.core.tests/AuthTests.cs

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
using System.IO;
88
using System.Linq;
99
using System.Security.Claims;
10+
using System.Text;
1011
using System.Threading;
1112
using System.Threading.Tasks;
1213
using DurableFunctionsMonitor.DotNetIsolated;
14+
using Newtonsoft.Json;
1315
using Microsoft.Azure.Functions.Worker;
1416
using Microsoft.IdentityModel.Tokens;
1517
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -641,5 +643,244 @@ public async Task ReturnsHubNameFromHostJsonAsCaseInsensitiveHashSet()
641643

642644
Assert.IsTrue(hubNames.Contains("mYtASKhUB"));
643645
}
646+
647+
[TestMethod]
648+
public async Task GetClaimsPrincipal_ReturnsPrincipalFromRequestIdentities()
649+
{
650+
// Arrange
651+
var request = new FakeHttpRequestData(new Uri("http://localhost"));
652+
var identity = new ClaimsIdentity(new[] {
653+
new Claim("preferred_username", "testuser@example.com")
654+
}, "TestAuthType");
655+
656+
request.AddIdentity(identity);
657+
var settings = new DfmSettings();
658+
659+
// Act
660+
var principal = await Auth.GetClaimsPrincipal(request, settings);
661+
662+
// Assert
663+
Assert.IsNotNull(principal);
664+
Assert.IsTrue(principal.Identity.IsAuthenticated);
665+
Assert.AreEqual("TestAuthType", principal.Identity.AuthenticationType);
666+
Assert.IsTrue(principal.HasClaim("preferred_username", "testuser@example.com"));
667+
}
668+
669+
[TestMethod]
670+
public async Task GetClaimsPrincipal_IdentityIsNotAuthenticated_ThrowsDfmUnauthorizedException()
671+
{
672+
// Arrange
673+
var request = new FakeHttpRequestData(new Uri("http://localhost"));
674+
// Create identity, but don't specify auth type, so it is not authenticated
675+
var identity = new ClaimsIdentity(new[] {
676+
new Claim("preferred_username", "testuser@example.com")
677+
});
678+
679+
request.AddIdentity(identity);
680+
var settings = new DfmSettings();
681+
682+
// Act & Assert
683+
var ex = await Assert.ThrowsExceptionAsync<DfmUnauthorizedException>(() => Auth.GetClaimsPrincipal(request, settings));
684+
Assert.AreEqual("No access token provided. Call is rejected.", ex.Message);
685+
}
686+
687+
[TestMethod]
688+
public async Task GetClaimsPrincipal_IdentityIsMissingUserNameClaim_ThrowsDfmUnauthorizedException()
689+
{
690+
// Arrange
691+
var request = new FakeHttpRequestData(new Uri("http://localhost"));
692+
// Authenticated identity, but without the required claim
693+
var identity = new ClaimsIdentity(new[] {
694+
new Claim("some_other_claim", "some_value")
695+
}, "TestAuthType");
696+
697+
request.AddIdentity(identity);
698+
var settings = new DfmSettings();
699+
700+
// Act & Assert
701+
var ex = await Assert.ThrowsExceptionAsync<DfmUnauthorizedException>(() => Auth.GetClaimsPrincipal(request, settings));
702+
Assert.AreEqual("No access token provided. Call is rejected.", ex.Message);
703+
}
704+
705+
[TestMethod]
706+
public async Task GetClaimsPrincipal_XMsClientPrincipalHeader_ReturnsPrincipalFromHeader()
707+
{
708+
string originalSiteName = Environment.GetEnvironmentVariable(EnvVariableNames.WEBSITE_SITE_NAME);
709+
string originalClientId = Environment.GetEnvironmentVariable(EnvVariableNames.WEBSITE_AUTH_CLIENT_ID);
710+
try
711+
{
712+
// Arrange
713+
Environment.SetEnvironmentVariable(EnvVariableNames.WEBSITE_SITE_NAME, "test-site");
714+
Environment.SetEnvironmentVariable(EnvVariableNames.WEBSITE_AUTH_CLIENT_ID, "test-client-id");
715+
716+
var request = new FakeHttpRequestData(new Uri("http://localhost"));
717+
718+
var clientPrincipal = new
719+
{
720+
auth_typ = "aad",
721+
claims = new[]
722+
{
723+
new { typ = "preferred_username", val = "testuser@example.com" },
724+
new { typ = "roles", val = "somerole" }
725+
}
726+
};
727+
728+
var clientPrincipalJson = JsonConvert.SerializeObject(clientPrincipal);
729+
var headerValue = Convert.ToBase64String(Encoding.UTF8.GetBytes(clientPrincipalJson));
730+
731+
request.Headers.Add("x-ms-client-principal", headerValue);
732+
733+
var settings = new DfmSettings();
734+
735+
// Act
736+
var principal = await Auth.GetClaimsPrincipal(request, settings);
737+
738+
// Assert
739+
Assert.IsNotNull(principal);
740+
Assert.AreEqual("aad", principal.Identity.AuthenticationType);
741+
Assert.IsTrue(principal.HasClaim("preferred_username", "testuser@example.com"));
742+
Assert.IsTrue(principal.HasClaim("roles", "somerole"));
743+
}
744+
finally
745+
{
746+
// Cleanup
747+
Environment.SetEnvironmentVariable(EnvVariableNames.WEBSITE_SITE_NAME, originalSiteName);
748+
Environment.SetEnvironmentVariable(EnvVariableNames.WEBSITE_AUTH_CLIENT_ID, originalClientId);
749+
}
750+
}
751+
752+
[TestMethod]
753+
public async Task GetClaimsPrincipal_XMsClientPrincipalHeaderNotValid_ThrowsDfmUnauthorizedException()
754+
{
755+
string originalSiteName = Environment.GetEnvironmentVariable(EnvVariableNames.WEBSITE_SITE_NAME);
756+
string originalClientId = Environment.GetEnvironmentVariable(EnvVariableNames.WEBSITE_AUTH_CLIENT_ID);
757+
try
758+
{
759+
// Arrange
760+
// Ensure env vars are not set
761+
Environment.SetEnvironmentVariable(EnvVariableNames.WEBSITE_SITE_NAME, null);
762+
Environment.SetEnvironmentVariable(EnvVariableNames.WEBSITE_AUTH_CLIENT_ID, null);
763+
764+
var request = new FakeHttpRequestData(new Uri("http://localhost"));
765+
766+
request.Headers.Add("x-ms-client-principal", "not-empty");
767+
768+
var settings = new DfmSettings();
769+
770+
// Act & Assert
771+
var ex = await Assert.ThrowsExceptionAsync<DfmUnauthorizedException>(() => Auth.GetClaimsPrincipal(request, settings));
772+
Assert.AreEqual("The incoming 'x-ms-client-principal' header is not legitimate. Call is rejected.", ex.Message);
773+
}
774+
finally
775+
{
776+
Environment.SetEnvironmentVariable(EnvVariableNames.WEBSITE_SITE_NAME, originalSiteName);
777+
Environment.SetEnvironmentVariable(EnvVariableNames.WEBSITE_AUTH_CLIENT_ID, originalClientId);
778+
}
779+
}
780+
781+
[TestMethod]
782+
public async Task GetClaimsPrincipal_XMsClientPrincipalHeaderMalformed_ThrowsDfmUnauthorizedException()
783+
{
784+
string originalSiteName = Environment.GetEnvironmentVariable(EnvVariableNames.WEBSITE_SITE_NAME);
785+
string originalClientId = Environment.GetEnvironmentVariable(EnvVariableNames.WEBSITE_AUTH_CLIENT_ID);
786+
try
787+
{
788+
// Arrange
789+
Environment.SetEnvironmentVariable(EnvVariableNames.WEBSITE_SITE_NAME, "test-site");
790+
Environment.SetEnvironmentVariable(EnvVariableNames.WEBSITE_AUTH_CLIENT_ID, "test-client-id");
791+
792+
var request = new FakeHttpRequestData(new Uri("http://localhost"));
793+
794+
request.Headers.Add("x-ms-client-principal", "this-is-not-base64");
795+
796+
var settings = new DfmSettings();
797+
798+
// Act & Assert
799+
var ex = await Assert.ThrowsExceptionAsync<DfmUnauthorizedException>(() => Auth.GetClaimsPrincipal(request, settings));
800+
Assert.IsTrue(ex.Message.StartsWith("Failed to parse the 'x-ms-client-principal' header."));
801+
}
802+
finally
803+
{
804+
// Cleanup
805+
Environment.SetEnvironmentVariable(EnvVariableNames.WEBSITE_SITE_NAME, originalSiteName);
806+
Environment.SetEnvironmentVariable(EnvVariableNames.WEBSITE_AUTH_CLIENT_ID, originalClientId);
807+
}
808+
}
809+
810+
[TestMethod]
811+
public async Task GetClaimsPrincipal_AuthorizationHeader_ReturnsPrincipalFromToken()
812+
{
813+
var originalJwtHandler = Auth.MockedJwtSecurityTokenHandler;
814+
var originalSigningKeysTask = Auth.GetSigningKeysTask;
815+
string originalClientId = Environment.GetEnvironmentVariable(EnvVariableNames.WEBSITE_AUTH_CLIENT_ID);
816+
string originalIssuer = Environment.GetEnvironmentVariable(EnvVariableNames.WEBSITE_AUTH_OPENID_ISSUER);
817+
try
818+
{
819+
// Arrange
820+
var request = new FakeHttpRequestData(new Uri("http://localhost"));
821+
822+
string userName = "tino@contoso.com";
823+
string roleName = "my-app-role";
824+
string audience = "my-audience";
825+
string issuer = "my-issuer";
826+
string token = "blah-blah";
827+
828+
var principal = new ClaimsPrincipal(new ClaimsIdentity(new[] {
829+
new Claim("preferred_username", userName),
830+
new Claim("roles", roleName)
831+
}, "TestAuthType"));
832+
833+
ICollection<SecurityKey> securityKeys = new SecurityKey[0];
834+
ValidateTokenDelegate validateTokenDelegate = (string t, TokenValidationParameters p, out SecurityToken st) =>
835+
{
836+
st = null;
837+
Assert.AreEqual(token, t);
838+
};
839+
840+
SecurityToken st = null;
841+
var jwtHandlerMoq = new Mock<JwtSecurityTokenHandler>();
842+
jwtHandlerMoq.Setup(h => h.ValidateToken(It.IsAny<string>(), It.IsAny<TokenValidationParameters>(), out st))
843+
.Callback(validateTokenDelegate)
844+
.Returns(principal);
845+
846+
Auth.MockedJwtSecurityTokenHandler = jwtHandlerMoq.Object;
847+
Auth.GetSigningKeysTask = Task.FromResult(securityKeys);
848+
849+
Environment.SetEnvironmentVariable(EnvVariableNames.WEBSITE_AUTH_CLIENT_ID, audience);
850+
Environment.SetEnvironmentVariable(EnvVariableNames.WEBSITE_AUTH_OPENID_ISSUER, issuer);
851+
852+
request.Headers.Add("Authorization", "Bearer " + token);
853+
var settings = new DfmSettings();
854+
855+
// Act
856+
var resultPrincipal = await Auth.GetClaimsPrincipal(request, settings);
857+
858+
// Assert
859+
Assert.IsNotNull(resultPrincipal);
860+
Assert.AreSame(principal, resultPrincipal);
861+
Assert.IsTrue(resultPrincipal.HasClaim("preferred_username", userName));
862+
Assert.IsTrue(resultPrincipal.HasClaim("roles", roleName));
863+
}
864+
finally
865+
{
866+
// Cleanup
867+
Auth.MockedJwtSecurityTokenHandler = originalJwtHandler;
868+
Auth.GetSigningKeysTask = originalSigningKeysTask;
869+
Environment.SetEnvironmentVariable(EnvVariableNames.WEBSITE_AUTH_CLIENT_ID, originalClientId);
870+
Environment.SetEnvironmentVariable(EnvVariableNames.WEBSITE_AUTH_OPENID_ISSUER, originalIssuer);
871+
}
872+
}
873+
874+
[TestMethod]
875+
public async Task GetClaimsPrincipal_ThrowsWhenNoAuthInfoProvided()
876+
{
877+
// Arrange
878+
var request = new FakeHttpRequestData(new Uri("http://localhost"));
879+
var settings = new DfmSettings();
880+
881+
// Act & Assert
882+
var ex = await Assert.ThrowsExceptionAsync<DfmUnauthorizedException>(() => Auth.GetClaimsPrincipal(request, settings));
883+
Assert.AreEqual("No access token provided. Call is rejected.", ex.Message);
884+
}
644885
}
645886
}

0 commit comments

Comments
 (0)