Skip to content
This repository was archived by the owner on Dec 13, 2018. It is now read-only.

Make claim mapping more configurable #1124

Merged
merged 1 commit into from
Mar 1, 2017
Merged

Make claim mapping more configurable #1124

merged 1 commit into from
Mar 1, 2017

Conversation

Tratcher
Copy link
Member

#969
@HaoK @blowdart @PinpointTownes

Thoughts?

@Tratcher Tratcher added this to the 2.0.0 milestone Feb 15, 2017
@Tratcher Tratcher self-assigned this Feb 15, 2017
@dnfclas
Copy link

dnfclas commented Feb 15, 2017

Hi @Tratcher, I'm your friendly neighborhood .NET Foundation Pull Request Bot (You can call me DNFBOT). Thanks for your contribution!
You've already signed the contribution license agreement. Thanks!

The agreement was validated by .NET Foundation and real humans are currently evaluating your PR.

TTYL, DNFBOT;

@kevinchalet
Copy link
Contributor

Thoughts?

Not sure I like it, but I can't think of a better approach, so... :trollface:

@Tratcher
Copy link
Member Author

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

@HaoK
Copy link
Member

HaoK commented Feb 15, 2017

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");
Copy link
Member

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?

Copy link
Member

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");

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JsonClaimsMap.Map[Nested]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClaimsResolver.Map

@HaoK
Copy link
Member

HaoK commented Feb 15, 2017

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.

@kevinchalet
Copy link
Contributor

IdentityModel already includes all of the claims from the JWTs, right?

Yep.

Related ticket: #1024


namespace Microsoft.AspNetCore.Authentication
{
public abstract class ClaimResolver<T>
Copy link
Member

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?

Copy link
Member Author

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"));
Copy link
Member

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

Copy link
Member Author

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.

Copy link
Member

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

@brentschmaltz
Copy link
Contributor

@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

@Tratcher
Copy link
Member Author

@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.

@HaoK
Copy link
Member

HaoK commented Feb 15, 2017

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 :)

@HaoK
Copy link
Member

HaoK commented Feb 15, 2017

ClaimMappings or PayloadClaimMappings also would read well with the current collection/AddXyz methods.

@brentschmaltz
Copy link
Contributor

@Tratcher perhaps I am misunderstanding, what will the ClaimReslover do? Add claims OR transform?

@Tratcher
Copy link
Member Author

@brentschmaltz it adds claims from the User Info

@Tratcher
Copy link
Member Author

Renamed. The funny thing is that I'd started with Map.

}
}
});
};
githubOptions.ClaimMaps.AddJsonKeyMap(ClaimTypes.NameIdentifier, "id");
Copy link
Member

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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MapFromJsonKey? MapFromJsonSubKey, MapFromCustomJson...?

Copy link
Member

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

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);
Copy link
Member

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();
Copy link
Member

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");
Copy link
Member

@HaoK HaoK Feb 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AddNestedJsonKeyMap => AddJsonSubKey?

Copy link
Member

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.

@Tratcher
Copy link
Member Author

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.

@HaoK
Copy link
Member

HaoK commented Feb 16, 2017

I figured as much, oh well :(

@HaoK
Copy link
Member

HaoK commented Feb 16, 2017

You could just put the shared classes in base Auth, we've done it before in the past I believe

@Tratcher
Copy link
Member Author

Not without adding the JSON dependency to the base class.

@HaoK
Copy link
Member

HaoK commented Feb 16, 2017

Yuck, can we shared source at least?

@Tratcher
Copy link
Member Author

Not for public types...

@Tratcher
Copy link
Member Author

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
Of those, I think I'd pick out these defaults:

  • sub, name, given_name, family_name, profile, email

@PinpointTownes, this was your pet feature in OIDC, what do you think about scaling it back?

@kevinchalet
Copy link
Contributor

kevinchalet commented Feb 17, 2017

This got my test cookie (an outlook.com account via AAD) down from 5,267 bytes broken into 2 chunks to 3,567 bytes with no chunking.

This sounds really large. How many claims do you have?

I suspect using the WS-Fed claim types here doesn't help.
FWIW, there's an ardent debate in the AzureAD repo about the IdentityModel claims mapping feature: AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet#550.

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.

@Tratcher
Copy link
Member Author

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.

@Tratcher Tratcher changed the title [WIP] Make social auth claims mapping more configurable Make claim mapping more configurable Feb 21, 2017
@Tratcher
Copy link
Member Author

@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?
See: https://github.com/aspnet/Security/pull/1124/files#diff-5f153b21e61d008634c479494de719d6

}
}
});
};
githubOptions.ClaimMaps.MapJsonKey(ClaimTypes.NameIdentifier, "id");
Copy link
Member

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
Copy link
Member

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
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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

Copy link
Member Author

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");
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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

Copy link
Member Author

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.

Copy link
Member

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...

@Tratcher
Copy link
Member Author

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.

@kevinchalet
Copy link
Contributor

@PinpointTownes, @brockallen, @leastprivilege, @brentschmaltz, is this a workable solution?

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");
Copy link
Contributor

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.

@Tratcher
Copy link
Member Author

@PinpointTownes what do you mean? I used the short names: https://github.com/aspnet/Security/pull/1124/files#diff-5f153b21e61d008634c479494de719d6R71.

@kevinchalet
Copy link
Contributor

@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?

@Tratcher
Copy link
Member Author

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));
Copy link
Member

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...

Copy link
Member

@HaoK HaoK Feb 23, 2017

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

Copy link
Member Author

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..

Copy link
Member

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?

Copy link
Member Author

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).

Copy link
Member

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();
Copy link
Member

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?

Copy link
Member

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");
Copy link
Member

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

Copy link
Member Author

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);
Copy link
Member

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

@HaoK
Copy link
Member

HaoK commented Feb 23, 2017

I like the usage, definitely way better than what we had before.

@kevinchalet
Copy link
Contributor

kevinchalet commented Feb 28, 2017

Sounds like the ClaimsTransformation component :-)

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 ClaimsTypes names I'm used to" mapping table in mind... 😄

@Tratcher Tratcher force-pushed the tratcher/resolvers branch from c2ae26b to ad42516 Compare March 1, 2017 18:56
@Tratcher Tratcher merged commit ad42516 into dev Mar 1, 2017
@Tratcher Tratcher deleted the tratcher/resolvers branch March 1, 2017 19:05
@natemcmaster
Copy link
Contributor

@Tratcher don't forget to update API usage in downstream repos, like MusicStore.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants