Skip to content

Commit

Permalink
Add more system claims and 'sub' support (#1916)
Browse files Browse the repository at this point in the history
  • Loading branch information
vicancy authored Feb 21, 2024
1 parent 504e2a3 commit fb60e1c
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 4 deletions.
35 changes: 31 additions & 4 deletions src/Microsoft.Azure.SignalR.Common/Utilities/ClaimsUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.Azure.SignalR.Protocol;
using Newtonsoft.Json.Linq;

namespace Microsoft.Azure.SignalR
{
Expand All @@ -18,7 +19,14 @@ internal static class ClaimsUtility
"aud", // Audience claim, used by service to make sure token is matched with target resource.
"exp", // Expiration time claims. A token is valid only before its expiration time.
"iat", // Issued At claim. Added by default. It is not validated by service.
"nbf" // Not Before claim. Added by default. It is not validated by service.
"nbf", // Not Before claim. Added by default. It is not validated by service.
"iss", // "iss" is a system claim. It is not validated by service.
"actort",
"acr",
"azp",
"c_hash",
"jti",
"nonce",
};

private static readonly ClaimsIdentity DefaultClaimsIdentity = new ClaimsIdentity();
Expand Down Expand Up @@ -115,14 +123,33 @@ public static IEnumerable<Claim> BuildJwtClaims(
}

// return customer's claims
var customerClaims = claimsProvider == null ? user?.Claims : claimsProvider.Invoke();
var customerClaims = (claimsProvider == null ? user?.Claims : claimsProvider.Invoke())?.ToArray();
if (customerClaims != null)
{
// According to the spec https://datatracker.ietf.org/doc/html/rfc7519#section-4.1
// the "sub" value is a case-sensitive string containing a StringOrURI value
// "sub" is used as the UserId if userId is not specified
// If "sub" exists, we here make sure only one "sub" is preserved, others will be renamed as user claim type
var hasSubClaim = false;
foreach (var claim in customerClaims)
{
if (claim.Type == "sub")
{
if (hasSubClaim)
{
// only the first "sub" is preserved as "sub", others will be renamed as user claims
yield return new Claim(Constants.ClaimType.AzureSignalRUserPrefix + claim.Type, claim.Value);
}
else
{
hasSubClaim = true;
// The first "sub" would be also considered as "nameIdentifier" and used as SignalR's UserIdentifier
yield return claim;
}

}
// Add AzureSignalRUserPrefix if customer's claim name is duplicated with SignalR system claims.
// And split it when return from SignalR Service.
if (SystemClaims.Contains(claim.Type))
else if (SystemClaims.Contains(claim.Type))
{
yield return new Claim(Constants.ClaimType.AzureSignalRUserPrefix + claim.Type, claim.Value);
}
Expand Down
75 changes: 75 additions & 0 deletions test/Microsoft.Azure.SignalR.Common.Tests/ClaimsUtilityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,80 @@ public void TestGetSystemClaims(ClaimsIdentity identity, string userId, Func<IEn

Assert.Equal(expectedClaimsCount, ci.Claims.Count());
}

[Fact]
public void TestGetPreservedSystemClaims()
{
// preserved system claims are renamed and reverted back
var claims = ClaimsUtility.BuildJwtClaims(
new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("iss", "A"), new Claim("jti", "B") })), null, null).ToArray();
Assert.Equal("asrs.u.iss", claims[0].Type);
Assert.Equal("asrs.u.jti", claims[1].Type);

var resultIdentity = ClaimsUtility.GetUserPrincipal(claims).Identity;

var ci = resultIdentity as ClaimsIdentity;
Assert.NotNull(ci);
Assert.Equal(2, ci.Claims.Count());
Assert.True(ci.HasClaim("iss", "A"));
Assert.True(ci.HasClaim("jti", "B"));
}

[Fact]
public void TestGetSubjectClaims()
{
// only the first sub claim is considered as valid to the service
var claims = ClaimsUtility.BuildJwtClaims(
new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("sub", "A"), new Claim("sub", "B") })), null, null).ToArray();
Assert.Equal("sub", claims[0].Type);
Assert.Equal("asrs.u.sub", claims[1].Type);

var resultIdentity = ClaimsUtility.GetUserPrincipal(claims).Identity;

var ci = resultIdentity as ClaimsIdentity;
Assert.NotNull(ci);
Assert.Equal(2, ci.Claims.Count());
Assert.True(ci.HasClaim("sub", "A"));
Assert.True(ci.HasClaim("sub", "B"));

claims = ClaimsUtility.BuildJwtClaims(
new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("sub", "A"), new Claim("sub", "B") })), "C", null).ToArray();
Assert.Equal("asrs.s.uid", claims[0].Type);
Assert.Equal("sub", claims[1].Type);
Assert.Equal("asrs.u.sub", claims[2].Type);

resultIdentity = ClaimsUtility.GetUserPrincipal(claims).Identity;

ci = resultIdentity as ClaimsIdentity;
Assert.NotNull(ci);
Assert.Equal(2, ci.Claims.Count());
Assert.True(ci.HasClaim("sub", "A"));
Assert.True(ci.HasClaim("sub", "B"));

// single sub claim is considered as valid
claims = ClaimsUtility.BuildJwtClaims(
new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("sub", "A") })), null, null).ToArray();
Assert.Single(claims);
Assert.Equal("sub", claims[0].Type);

resultIdentity = ClaimsUtility.GetUserPrincipal(claims).Identity;

ci = resultIdentity as ClaimsIdentity;
Assert.NotNull(ci);
Assert.Single(ci.Claims);
Assert.True(ci.HasClaim("sub", "A"));

claims = ClaimsUtility.BuildJwtClaims(
new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("sub", "A") })), "C", null).ToArray();
Assert.Equal("asrs.s.uid", claims[0].Type);
Assert.Equal("sub", claims[1].Type);

resultIdentity = ClaimsUtility.GetUserPrincipal(claims).Identity;

ci = resultIdentity as ClaimsIdentity;
Assert.NotNull(ci);
Assert.Single(ci.Claims);
Assert.True(ci.HasClaim("sub", "A"));
}
}
}

0 comments on commit fb60e1c

Please sign in to comment.