-
Notifications
You must be signed in to change notification settings - Fork 434
Serialize and deserialize actor token in claims identity #3219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
105e513
b5c02e6
90d5667
2a0c1b1
9b0c1bd
b7a2280
7b65a01
cd264f5
a7b578d
7a657e9
04257cd
d354200
ff64ddd
f66c4d0
5dc4a49
ccb0688
92a6285
99f7fd5
4185264
8dcbdb3
89cd0e4
3ad51eb
3f348bc
787099b
a92b6d6
36623ea
e3c25f9
e20d067
d531454
d79c61b
f7de606
f3c6ed2
4185c6f
4c57aaf
a585112
e6638ae
95db310
9a1f6ed
5e10aec
3b21635
a7398a8
a8efcb8
4d2770f
bd78d2e
9503403
78cc3bc
bbb2f02
e7952d1
e3e47ee
06b5820
1dfda65
6caaae5
7cbe282
8eede12
e5ce2bc
e28773c
badf08a
2c23923
2ab6ef6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -668,11 +668,16 @@ internal static void WriteJwsPayload( | |||||
| // Duplicates are resolved according to the following priority: | ||||||
| // SecurityTokenDescriptor.{Audience/Audiences, Issuer, Expires, IssuedAt, NotBefore}, SecurityTokenDescriptor.Claims, SecurityTokenDescriptor.Subject.Claims | ||||||
| // SecurityTokenDescriptor.Claims are KeyValuePairs<string,object>, whereas SecurityTokenDescriptor.Subject.Claims are System.Security.Claims.Claim and are processed differently. | ||||||
|
|
||||||
| bool isActorFound = false; | ||||||
| if (tokenDescriptor.Claims != null && tokenDescriptor.Claims.Count > 0) | ||||||
| { | ||||||
| foreach (KeyValuePair<string, object> kvp in tokenDescriptor.Claims) | ||||||
| { | ||||||
| if (kvp.Key.Equals(tokenDescriptor.ActorClaimType, StringComparison.Ordinal)) | ||||||
| { | ||||||
| isActorFound = true; | ||||||
| continue; | ||||||
| } | ||||||
| if (!descriptorClaimsAudienceChecked && kvp.Key.Equals(JwtRegisteredClaimNames.Aud, StringComparison.Ordinal)) | ||||||
| { | ||||||
| descriptorClaimsAudienceChecked = true; | ||||||
|
|
@@ -754,6 +759,8 @@ internal static void WriteJwsPayload( | |||||
| JsonPrimitives.WriteObject(ref writer, kvp.Key, kvp.Value); | ||||||
| } | ||||||
| } | ||||||
| if (isActorFound || tokenDescriptor.Subject?.Actor != null) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should be: !isActorFound |
||||||
| WriteActorToken(writer, tokenDescriptor, setDefaultTimesOnTokenCreation, tokenLifetimeInMinutes); | ||||||
|
|
||||||
| AddSubjectClaims(ref writer, tokenDescriptor, audienceSet, issuerSet, ref expSet, ref iatSet, ref nbfSet); | ||||||
|
|
||||||
|
|
@@ -1071,6 +1078,74 @@ internal static byte[] WriteJweHeader(SecurityTokenDescriptor tokenDescriptor) | |||||
| } | ||||||
| } | ||||||
| } | ||||||
| internal static void WriteActorToken( | ||||||
| Utf8JsonWriter writer, | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is important to pass the writer using the 'ref' keyword.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will do this thank you for your review @brentschmaltz |
||||||
| SecurityTokenDescriptor tokenDescriptor, | ||||||
| bool setDefaultTimesOnTokenCreation, | ||||||
| int tokenLifetimeInMinutes) | ||||||
| { | ||||||
| if (tokenDescriptor == null) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tokenDescriptor should never be null here, you can check for null, but don't throw, just return.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, will do this! |
||||||
| throw new ArgumentNullException(nameof(tokenDescriptor)); | ||||||
|
|
||||||
| var actorTokenDescriptor = CreateActorTokenDescriptor(tokenDescriptor); | ||||||
| if (actorTokenDescriptor == null || actorTokenDescriptor.Subject == null) | ||||||
| return; | ||||||
|
|
||||||
| writer.WritePropertyName(tokenDescriptor.ActorClaimType); | ||||||
saurabhsathe-ms marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| WriteJwsPayload(ref writer, actorTokenDescriptor, setDefaultTimesOnTokenCreation, tokenLifetimeInMinutes); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can simply this method by just passing the ClaimsIdentity, and the writing the claims into JSON. The parameters to this method could be. internal static void WriteActorToken(
ref Utf8JsonWriter writer,
string claimName,
ClaimsIdentity claimsIdentity) |
||||||
| } | ||||||
|
|
||||||
| private static void ValidateActorChainDepth(SecurityTokenDescriptor tokenDescriptor) | ||||||
| { | ||||||
| if (tokenDescriptor.ActorChainDepth >= tokenDescriptor.MaxActorChainLength) | ||||||
| { | ||||||
| throw LogHelper.LogExceptionMessage( | ||||||
| new SecurityTokenException(LogHelper.FormatInvariant( | ||||||
| LogMessages.IDX14313, | ||||||
| LogHelper.MarkAsNonPII(tokenDescriptor.ActorChainDepth), | ||||||
| LogHelper.MarkAsNonPII(tokenDescriptor.MaxActorChainLength)))); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| private static SecurityTokenDescriptor CreateActorTokenDescriptor(SecurityTokenDescriptor tokenDescriptor) | ||||||
| { | ||||||
| SecurityTokenDescriptor actorTokenDescriptor = null; | ||||||
|
|
||||||
| if (tokenDescriptor.Claims?.TryGetValue(tokenDescriptor.ActorClaimType, out object actorValue) == true) | ||||||
| { | ||||||
| if (actorValue is not ClaimsIdentity actor) | ||||||
| { | ||||||
| throw LogHelper.LogExceptionMessage(new SecurityTokenException( | ||||||
| LogHelper.FormatInvariant( | ||||||
| LogMessages.IDX14315, | ||||||
| LogHelper.MarkAsNonPII(tokenDescriptor.ActorClaimType), | ||||||
| LogHelper.MarkAsNonPII(actorValue?.GetType().ToString() ?? "null")))); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| } | ||||||
|
|
||||||
| actorTokenDescriptor = new SecurityTokenDescriptor | ||||||
| { | ||||||
| Subject = actor, | ||||||
| }; | ||||||
|
|
||||||
| } | ||||||
| // Then check for actor in subject | ||||||
| else if (tokenDescriptor.Subject?.Actor != null) | ||||||
| { | ||||||
| actorTokenDescriptor = new SecurityTokenDescriptor | ||||||
| { | ||||||
| Subject = tokenDescriptor.Subject.Actor, | ||||||
| }; | ||||||
| } | ||||||
| if (actorTokenDescriptor != null) | ||||||
| { | ||||||
| ValidateActorChainDepth(tokenDescriptor); | ||||||
| actorTokenDescriptor.MaxActorChainLength = tokenDescriptor.MaxActorChainLength; | ||||||
| actorTokenDescriptor.ActorClaimType = tokenDescriptor.ActorClaimType; | ||||||
| actorTokenDescriptor.ActorChainDepth = tokenDescriptor.ActorChainDepth + 1; | ||||||
| } | ||||||
|
|
||||||
| return actorTokenDescriptor; | ||||||
| } | ||||||
|
|
||||||
| internal static byte[] CompressToken(byte[] utf8Bytes, string compressionAlgorithm) | ||||||
| { | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |||||
| using System; | ||||||
| using System.Collections.Generic; | ||||||
| using System.Security.Claims; | ||||||
| using System.Text.Json; | ||||||
| using System.Text.RegularExpressions; | ||||||
| using System.Threading; | ||||||
| using System.Threading.Tasks; | ||||||
|
|
@@ -212,8 +213,8 @@ protected virtual ClaimsIdentity CreateClaimsIdentity(JsonWebToken jwtToken, Tok | |||||
|
|
||||||
| private ClaimsIdentity CreateClaimsIdentityWithMapping(JsonWebToken jwtToken, TokenValidationParameters validationParameters, string issuer) | ||||||
| { | ||||||
| _ = validationParameters ?? throw LogHelper.LogArgumentNullException(nameof(validationParameters)); | ||||||
|
|
||||||
| _ = validationParameters ?? throw LogHelper.LogArgumentNullException(nameof(validationParameters)); | ||||||
| ClaimsIdentity identity = validationParameters.CreateClaimsIdentity(jwtToken, issuer); | ||||||
| foreach (Claim jwtClaim in jwtToken.Claims) | ||||||
| { | ||||||
|
|
@@ -222,19 +223,14 @@ private ClaimsIdentity CreateClaimsIdentityWithMapping(JsonWebToken jwtToken, To | |||||
| if (!wasMapped) | ||||||
| claimType = jwtClaim.Type; | ||||||
|
|
||||||
| if (claimType == ClaimTypes.Actor) | ||||||
| if (claimType.Equals(validationParameters.ActorClaimType) || claimType.Equals("actort")) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||||||
| { | ||||||
| if (identity.Actor != null) | ||||||
| throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant( | ||||||
| LogMessages.IDX14112, | ||||||
| LogHelper.MarkAsNonPII(JwtRegisteredClaimNames.Actort), | ||||||
| LogHelper.MarkAsNonPII(claimType), | ||||||
| jwtClaim.Value))); | ||||||
|
|
||||||
| if (CanReadToken(jwtClaim.Value)) | ||||||
| { | ||||||
| JsonWebToken actor = ReadToken(jwtClaim.Value) as JsonWebToken; | ||||||
| identity.Actor = CreateClaimsIdentity(actor, validationParameters); | ||||||
| } | ||||||
| identity.Actor = CreateClaimsIdentityActor(jwtToken, jwtClaim.Value, validationParameters, claimType.Equals(validationParameters.ActorClaimType)); | ||||||
| } | ||||||
|
|
||||||
| if (wasMapped) | ||||||
|
|
@@ -287,16 +283,11 @@ private ClaimsIdentity CreateClaimsIdentityPrivate(JsonWebToken jwtToken, TokenV | |||||
| foreach (Claim jwtClaim in jwtToken.Claims) | ||||||
| { | ||||||
| string claimType = jwtClaim.Type; | ||||||
| if (claimType == ClaimTypes.Actor) | ||||||
| if (claimType == validationParameters.ActorClaimType || claimType.Equals("actort")) | ||||||
| { | ||||||
| if (identity.Actor != null) | ||||||
| throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(LogMessages.IDX14112, LogHelper.MarkAsNonPII(JwtRegisteredClaimNames.Actort), jwtClaim.Value))); | ||||||
|
|
||||||
| if (CanReadToken(jwtClaim.Value)) | ||||||
| { | ||||||
| JsonWebToken actor = ReadToken(jwtClaim.Value) as JsonWebToken; | ||||||
| identity.Actor = CreateClaimsIdentity(actor, validationParameters, issuer); | ||||||
| } | ||||||
| throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(LogMessages.IDX14112, LogHelper.MarkAsNonPII(claimType), jwtClaim.Value))); | ||||||
| identity.Actor = CreateClaimsIdentityActor(jwtToken, jwtClaim.Value, validationParameters, claimType.Equals(validationParameters.ActorClaimType)); | ||||||
| } | ||||||
|
|
||||||
| if (jwtClaim.Properties.Count == 0) | ||||||
|
|
@@ -621,5 +612,133 @@ private static TokenValidationResult ReadToken(string token, TokenValidationPara | |||||
| IsValid = true | ||||||
| }; | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Creates a ClaimsIdentity from an actor claim string. | ||||||
| /// </summary> | ||||||
| /// <param name="jwtToken"></param> | ||||||
| /// <param name="actorString">The actor claim string.</param> | ||||||
| /// <param name="tokenValidationParameters">The token validation parameters.</param> | ||||||
| /// <param name="isStandardAct">This tells us if we want to deserialize it as a JWT or Json Object. If this is set to true then we deserialize as JsonObject else as JWT </param> | ||||||
| /// <returns>A ClaimsIdentity representing the actor.</returns> | ||||||
| /// <exception cref="ArgumentNullException">Thrown if <paramref name="actorString"/> or <paramref name="tokenValidationParameters"/> is null.</exception> | ||||||
| private ClaimsIdentity CreateClaimsIdentityActor( | ||||||
| JsonWebToken jwtToken, | ||||||
| string actorString, | ||||||
| TokenValidationParameters tokenValidationParameters, | ||||||
| bool isStandardAct = false) | ||||||
| { | ||||||
| if (string.IsNullOrEmpty(actorString)) | ||||||
| throw LogHelper.LogArgumentNullException(nameof(actorString)); | ||||||
|
|
||||||
| if (tokenValidationParameters == null) | ||||||
| throw LogHelper.LogArgumentNullException(nameof(tokenValidationParameters)); | ||||||
|
|
||||||
| if (isStandardAct) | ||||||
| { | ||||||
| if (jwtToken.TryGetPayloadValue<JsonElement>(tokenValidationParameters.ActorClaimType, out JsonElement actClaim)) | ||||||
| { | ||||||
| if (tokenValidationParameters.ActClaimRetrieverDelegate != null) | ||||||
| { | ||||||
| try | ||||||
| { | ||||||
| return tokenValidationParameters.ActClaimRetrieverDelegate(actClaim, tokenValidationParameters); | ||||||
| } | ||||||
| catch (Exception ex) | ||||||
| { | ||||||
| throw LogHelper.LogExceptionMessage(new SecurityTokenDecryptionFailedException(LogHelper.FormatInvariant( | ||||||
| LogMessages.IDX14314, | ||||||
| LogHelper.MarkAsNonPII(ex.ToString())))); | ||||||
| } | ||||||
| } | ||||||
| else | ||||||
| { | ||||||
| return CreateActorClaimsIdentityFromJsonElement(actClaim, tokenValidationParameters); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| } | ||||||
| else | ||||||
| { | ||||||
| if (CanReadToken(actorString)) | ||||||
| { | ||||||
| JsonWebToken actor = ReadToken(actorString) as JsonWebToken; | ||||||
| return CreateClaimsIdentity(actor, tokenValidationParameters); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| return null; | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Creates a ClaimsIdentity from a JsonElement that represents an actor token. | ||||||
| /// </summary> | ||||||
| /// <param name="jsonElement">The JsonElement containing actor claims.</param> | ||||||
| /// <param name="tokenValidationParameters">These parameters have details like nested actor chain length and max permissible actor length</param> | ||||||
| /// <param name="issuer">The issuer for the claims.</param> | ||||||
| /// <returns>A ClaimsIdentity containing claims from the JsonElement.</returns> | ||||||
| public static ClaimsIdentity CreateActorClaimsIdentityFromJsonElement( | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be private. |
||||||
| JsonElement jsonElement, | ||||||
| TokenValidationParameters tokenValidationParameters, | ||||||
| string issuer = null) | ||||||
| { | ||||||
| if (tokenValidationParameters == null) | ||||||
| throw LogHelper.LogArgumentNullException(nameof(tokenValidationParameters)); | ||||||
|
|
||||||
| if (tokenValidationParameters.ActorChainDepth >= tokenValidationParameters.MaxActorChainLength) | ||||||
| { | ||||||
| throw LogHelper.LogExceptionMessage( | ||||||
| new SecurityTokenException(LogHelper.FormatInvariant( | ||||||
| LogMessages.IDX14313, | ||||||
| LogHelper.MarkAsNonPII(tokenValidationParameters.ActorChainDepth), | ||||||
| LogHelper.MarkAsNonPII(tokenValidationParameters.MaxActorChainLength)))); | ||||||
| } | ||||||
|
|
||||||
| if (jsonElement.ValueKind != JsonValueKind.Object) | ||||||
| throw LogHelper.LogExceptionMessage(new ArgumentException("Actor token must be a JSON object")); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use IDX error message? |
||||||
|
|
||||||
| // Use CaseSensitiveClaimsIdentity for consistent behavior with the rest of the library | ||||||
| var identity = new CaseSensitiveClaimsIdentity(); | ||||||
|
|
||||||
| issuer = issuer ?? ClaimsIdentity.DefaultIssuer; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| foreach (var property in jsonElement.EnumerateObject()) | ||||||
| { | ||||||
| string claimType = property.Name; | ||||||
| JsonElement value = property.Value; | ||||||
|
|
||||||
| // Special handling for nested actor claim | ||||||
| if (claimType == tokenValidationParameters.ActorClaimType) | ||||||
| { | ||||||
| if (value.ValueKind == JsonValueKind.Object) | ||||||
| { | ||||||
| tokenValidationParameters.ActorChainDepth++; | ||||||
| // Recursively create nested actor identity | ||||||
| identity.Actor = CreateActorClaimsIdentityFromJsonElement( | ||||||
| value, tokenValidationParameters, issuer); | ||||||
| } | ||||||
| continue; | ||||||
| } | ||||||
|
|
||||||
| // For all other claims, create and add them | ||||||
| if (value.ValueKind == JsonValueKind.Array) | ||||||
| { | ||||||
| foreach (JsonElement element in value.EnumerateArray()) | ||||||
| { | ||||||
| var claim = JsonClaimSet.CreateClaimFromJsonElement(claimType, issuer, element); | ||||||
| if (claim != null) | ||||||
| identity.AddClaim(claim); | ||||||
| } | ||||||
| } | ||||||
| else | ||||||
| { | ||||||
| var claim = JsonClaimSet.CreateClaimFromJsonElement(claimType, issuer, value); | ||||||
| if (claim != null) | ||||||
| identity.AddClaim(claim); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| return identity; | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.DecryptTokenWithConfigurationAsync(Microsoft.IdentityModel.JsonWebTokens.JsonWebToken jwtToken, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<string> | ||
| static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.CreateActorClaimsIdentityFromJsonElement(System.Text.Json.JsonElement jsonElement, Microsoft.IdentityModel.Tokens.TokenValidationParameters tokenValidationParameters, string issuer = null) -> System.Security.Claims.ClaimsIdentity | ||
| Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.DecryptTokenWithConfigurationAsync(Microsoft.IdentityModel.JsonWebTokens.JsonWebToken jwtToken, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<string> |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following the logic for duplicate claims, we should write out the 'actor' claim, not sure why we continue.