-
Notifications
You must be signed in to change notification settings - Fork 598
Conversation
Hi @Tratcher, I'm your friendly neighborhood .NET Foundation Pull Request Bot (You can call me DNFBOT). Thanks for your contribution! The agreement was validated by .NET Foundation and real humans are currently evaluating your PR. TTYL, DNFBOT; |
Not sure I like it, but I can't think of a better approach, so... |
I was looking at how this would apply to OIDC and realized that right now all possible data from the user-info is being added as claims. No wonder OIDC has cookie size issues. Would it make sense to replace that system with this new explicit mapping? It would likely require a little more setup for devs, and I don't know if there are any default mappings it would make sense to include. IdentityModel already includes all of the claims from the JWTs, right? /cc: @brentschmaltz |
Not in love with the resolver name, I'll chew on this a bit |
ClaimResolvers.Add(ClaimTypes.NameIdentifier, "id"); | ||
ClaimResolvers.Add(ClaimTypes.Name, "displayName"); | ||
ClaimResolvers.AddNested(ClaimTypes.GivenName, "name", "givenName"); | ||
ClaimResolvers.AddNested(ClaimTypes.Surname, "name", "familyName"); |
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.
What does nested do exactly?
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.
Just brainstorming some different ways to express this:
JObjectTransform.Map(ClaimTypes.NameIdentifier, "id");
JObjectTransform.Map(ClaimTypes.Name, "displayName");
JObjectTransform.MapNested(ClaimTypes.GivenName, "name", "givenName");
JObjectTransform.MapNeste(ClaimTypes.Surname, "name", "familyName");
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.
JsonClaimsMap.Map[Nested]
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.
ClaimsResolver.Map
Yeah so my initial instinct is that this feels pretty heavy, but maybe if we just tweak a few names it won't feel so complicated. |
Yep. Related ticket: #1024 |
|
||
namespace Microsoft.AspNetCore.Authentication | ||
{ | ||
public abstract class ClaimResolver<T> |
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.
Is this going to be used outside of OAuth? Should we target this against OAuth/JObject for now?
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.
Trying to see if it applies to Twitter, OIDC, etc.. We'll see.
// Last one wins, if present | ||
ClaimResolvers.Add(ClaimTypes.Email, "userPrincipalName"); | ||
ClaimResolvers.Add(ClaimTypes.Email, "mail"); | ||
ClaimResolvers.AddCustom(ClaimTypes.Email, user => user.Value<string>("mail") ?? user.Value<string>("userPrincipalName")); |
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.
Honestly I almost prefer if we didn't have the sugar and only exposed the lambda syntax to be clear, its super obvious what's going on that way and we can expose helpers as needed to make the nested stuff nicer
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.
Really? I thought the new version was supper clean and easy. It sounds like you want some more explicit names though.
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.
Well for code like this I'd prefer if it was self describing, i.e. if it was JsonPayloadClaimsMap.Add("ClaimsTypes.Email", "userPrincipalName) that has much more context
@Tratcher @HaoK @PinpointTownes I am not clear on what problem are we trying to solve. Pinpoint referred us to #1024 . Which combined with AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet#550 would reduce the size of cookies. I am not a fan of mapping claims in the JWT as the claim types have strong definitions https://tools.ietf.org/html/rfc7519#section-4.1 |
@brentschmaltz see #969. We often get requests to map additional user data to claims during auth. Today if you want to add any you have to hook the events do everything yourself. This is too much for basic users and they ask to add it for them. We don't want to maintain an exhaustive list, especially since they keep changing. This PR is to make it easier for devs to add/change claim mappings themselves. It makes our own implementation cleaner too. |
My first suggestion for naming is: Resolver => Mapper which gets you the additional Apply => Map. I'd suggest Transform but that will collide with the real ClaimsTransformation feature... unless we call it PayloadClaimsTransform :) |
ClaimMappings or PayloadClaimMappings also would read well with the current collection/AddXyz methods. |
@Tratcher perhaps I am misunderstanding, what will the ClaimReslover do? Add claims OR transform? |
@brentschmaltz it adds claims from the User Info |
Renamed. The funny thing is that I'd started with Map. |
samples/SocialSample/Startup.cs
Outdated
} | ||
} | ||
}); | ||
}; | ||
githubOptions.ClaimMaps.AddJsonKeyMap(ClaimTypes.NameIdentifier, "id"); |
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.
Since the property is ClaimsMaps, can maybe simplify to AddJsonKey?
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.
MapFromJsonKey? MapFromJsonSubKey, MapFromCustomJson...?
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.
I don't think the From adds anything does it? Map[Custom]Json[Sub]Key works for me
samples/SocialSample/Startup.cs
Outdated
githubOptions.ClaimMaps.AddJsonKeyMap(ClaimTypes.NameIdentifier, "id"); | ||
githubOptions.ClaimMaps.AddJsonKeyMap(ClaimTypes.Name, "login"); | ||
githubOptions.ClaimMaps.AddJsonKeyMap("urn:github:name", "name"); | ||
githubOptions.ClaimMaps.AddJsonKeyMap(ClaimTypes.Email, "email", ClaimValueTypes.Email); |
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.
Can call this AddJsonSubKey if you want to preserve the different method for subsections if you want?
{ | ||
identity.AddClaim(new Claim("urn:facebook:timezone", timeZone, ClaimValueTypes.String, Options.ClaimsIssuer)); | ||
} | ||
context.ResolveClaims(); |
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.
MapClaims?
@@ -30,6 +32,21 @@ public FacebookOptions() | |||
Fields.Add("email"); | |||
Fields.Add("first_name"); | |||
Fields.Add("last_name"); | |||
|
|||
ClaimMaps.AddJsonKeyMap(ClaimTypes.NameIdentifier, "id"); | |||
ClaimMaps.AddNestedJsonKeyMap("urn:facebook:age_range_min", "age_range", "min"); |
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.
AddNestedJsonKeyMap => AddJsonSubKey?
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.
I think subkey is more common than nested.
Drat, to add this to Twitter and OIDC I have to clone all the classes. They all depend on Newtonsoft.Json but don't share any dependencies that require it. The alternative is to make Twitter and OIDC depend on OAuth, which makes sense from a protocol perspective but not really from a packaging perspective. |
I figured as much, oh well :( |
You could just put the shared classes in base Auth, we've done it before in the past I believe |
Not without adding the JSON dependency to the base class. |
Yuck, can we shared source at least? |
Not for public types... |
Changed Twitter to depend on OAuth. Mostly harmless. I could do the same for OIDC, but there's a larger question on if we want to use this pattern for OIDC. Right now OIDC maps all user data to claims by default. In hindsight this is overkill and you end up with a bunch of junk claims. OIDC also gets claims from the JWT and needs to de-duplicate (with preference given to the JWT claims). I'd need a MapUnique... variant. OIDC defines some standard claims: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
@PinpointTownes, this was your pet feature in OIDC, what do you think about scaling it back? |
This sounds really large. How many claims do you have? I suspect using the WS-Fed claim types here doesn't help. Disabling this feature by default and using the JWT claim types returned by the OP as-is would likely help reduce the size of the authentication cookies, but of course, this decision has its own downsides, as every external middleware will now have its own claim types, depending on the standard they implement. |
I had 21 claims before and 10 after. Yes, the long claim types contribute quite a bit to the size. Using the short oidc terms for ClaimType doesn't help (yet) because the claims coming from the JWT take precedence. |
@HaoK I think this is ready for a formal review. The only open question is if this also satisfies #1024 around removing some of the OIDC protocol claims that IM adds by default. @PinpointTownes, @brockallen, @leastprivilege, @brentschmaltz, is this a workable solution? |
samples/SocialSample/Startup.cs
Outdated
} | ||
} | ||
}); | ||
}; | ||
githubOptions.ClaimMaps.MapJsonKey(ClaimTypes.NameIdentifier, "id"); |
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.
Would we lose any meaning if we called the property Claims since all of the methods are called Map? Just looks a bit odd for it to be ClaimsMaps.Map everywhere?
/// <summary> | ||
/// A JsonClaimMapper that deletes all claims from the given ClaimsIdentity with the given ClaimType. | ||
/// </summary> | ||
public class DeleteClaimMapper : JsonClaimMapper |
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.
A delete mapper makes me think we should choose a different word.
like ClaimAction/Operation? with Map/Delete becoming something paired?
Copy/Delete or Add/Remove
Claims.CopyJsonKey(ClaimTypes.NameIdentifier, "id");
Claims.Delete("ClaimTypeToDelete");
|
||
namespace Microsoft.AspNetCore.Authentication | ||
{ | ||
public static class JsonClaimMapperCollectionExtensions |
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.
I'm not sure I like this as extension methods on an IEnumerable.
What do you think about making the public API into a ClaimsManager instead, with similar API? The property is then just named ClaimsManager.
ClaimsManager.CopyJsonKey(ClaimsTypes.NameIdentifier, "id");
ClaimsManager.Delete(ClaimsTypes.ReallyLongStuffIDon'tWant);
You can still have these methods add some kind of strongly typed Action that maps to the Map classes, or directly add an Action with the signature of Map. I don't think the extensiblity changes much as they could still add extension methods to the Manager just like the collection.
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.
What does the IEnumerable have to do with anything? The extension methods are for the JsonClaimMapperCollection and they all wrap the Add method.
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.
Sorry what I meant was instead of a JsonClaimMapperCollection, maybe that should be called something like ClaimsManager which implies that its got more than just basic add/remove
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.
It only has basic AddRemove. I renamed it to ClaimActionCollection and the property is called ClaimActions.
ClaimActions.Map...
ClaimActions.DeleteClaim...
@@ -55,6 +56,24 @@ public OpenIdConnectOptions(string authenticationScheme) | |||
Events = new OpenIdConnectEvents(); | |||
Scope.Add("openid"); | |||
Scope.Add("profile"); | |||
|
|||
ClaimMaps.DeleteClaim("nonce"); |
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.
Maybe add sugar to pass in a params string[] of claims to delete?
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.
We try to avoid params in public APIs. We'll see how often this gets used externally.
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.
What's the rationale against params? We certainly have them in several places today already for exactly this situation, truncating a lot of duplicated lines, maybe you don't even really need DeleteClaim, just a single params DeleteClaims
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.
When you have params on a method it makes it hard to ever add other inputs to that method.
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.
That's what overloads are for :) I'd have the params method for this usage alone personally...
More renames :-). ClaimMappers are now ClaimActions, I also made a few changes to make sure that the actions run even when user-info retrieval is disabled (Twitter, OIDC), and that the rules are hardened against null user info. |
I'd bet a lot of money @leastprivilege won't like the fact the JWT claims are magically converted to their WS-Fed equivalent, specially since he'll now have to disable that in two places: in IdentityModel and in ASP.NET Core 😅 |
ClaimActions.DeleteClaim("iat"); | ||
ClaimActions.DeleteClaim("nbf"); | ||
ClaimActions.DeleteClaim("exp"); | ||
ClaimActions.DeleteClaim("c_hash"); |
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.
You can also add at_hash
, auth_time
, acr
, amr
and azp
.
@PinpointTownes what do you mean? I used the short names: https://github.com/aspnet/Security/pull/1124/files#diff-5f153b21e61d008634c479494de719d6R71. |
@Tratcher ah, my bad, I had not realized you had changed that in the latest commits 😅 Now, the question is: do we want to generalize that to the other middleware? Also, do we want to have a global claims transformer that would convert all the claim types to a unique standard, so the application code wouldn't have to deal with multiple specs? |
Sounds like the ClaimsTransformation component :-) |
/// <param name="valueType">The value to use for Claim.ValueType when creating a Claim.</param> | ||
public static void MapUniqueJsonKey(this ClaimActionCollection collection, string claimType, string jsonKey, string valueType) | ||
{ | ||
collection.Add(new UniqueJsonKeyClaimAction(claimType, valueType, jsonKey)); |
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.
So my earlier point about the Collection vs Manager wasn't so much about that name, but that it seems unnecessary to have public types that are only used in the extension method. Why can't we just bake the logic directly into Method instead of having the method and a type (that's just a class around a Action. Cleans up the public surface area, and I don't see much use for the action types outside of the methods. Which lead me to ask why we shouldn't just call it a Manager with the methods, instead of a List of action thingies...
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.
Basically today it looks like there's almost two ways to use the feature (adding a custom Action object to the Collection, or using the sugar Map/Delete methods).
Given that there's already the MapCustom which basically serves the same purpose adding a custom action would, I think we should just eliminate all of the Action classes and only have the Collection API
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.
Yes you could implement all of the rules with MapCustom, but that comes down to a style choice. The classes provide some minor code cleanliness benefits like inherited properties, better stack traces, avoids closures, etc..
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.
Fair enough, but do the classes need to be public? Are you expecting them to be used other than in the explicit methods?
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.
I'd expect some new types to be derived from the ones provided. We could dump them into an infrastructure namespace (e.g. Oath.Claims).
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.
Cool moving them all into their own namespace works
{ | ||
identity.AddClaim(new Claim(ClaimTypes.Email, email, ClaimValueTypes.String, Options.ClaimsIssuer)); | ||
} | ||
context.MapClaims(); |
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.
MapClaims => RunActions/InvokeActions/ExecuteActions/FireWhenReady?
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.
InvokeClaimsActions
@@ -23,6 +25,12 @@ public MicrosoftAccountOptions() | |||
TokenEndpoint = MicrosoftAccountDefaults.TokenEndpoint; | |||
UserInformationEndpoint = MicrosoftAccountDefaults.UserInformationEndpoint; | |||
Scope.Add("https://graph.microsoft.com/user.read"); | |||
|
|||
ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id"); |
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.
Now that mapper is gone from everywhere else, maybe Import/Copy is better, since map is kinda vague what we are mapping.
CopyJsonKey
ImportJsonKey
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.
I think Map still works. Map JSON Key to Claim
/// <param name="userData">The source data to exhamine. This value may be null.</param> | ||
/// <param name="identity">The identity to add Claims to.</param> | ||
/// <param name="issuer">The value to use for Claim.Issuer when creating a Claim.</param> | ||
public abstract void Run(JObject userData, ClaimsIdentity identity, string issuer); |
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.
Yeah I think rather than making this something classes implement, this could just be an Action<JObject, ClaimsIdentity, string>Action
property, which the extension methods set. So the change could be to move the Run implementations into the extension methods as property setters on the single ClaimsAction class
I like the usage, definitely way better than what we had before. |
Yeah, but it's totally useless OOTB if you don't provide your own handler. I had more something like a "convert all these JWT/SWT/WS-Fed/custom claims to the |
59a0018
to
d857f49
Compare
#1024 Remove OIDC protocol claims
c2ae26b
to
ad42516
Compare
@Tratcher don't forget to update API usage in downstream repos, like MusicStore. |
#969
@HaoK @blowdart @PinpointTownes
Thoughts?