-
Notifications
You must be signed in to change notification settings - Fork 597
Save tokens in auth properties instead of claims #698
Conversation
Hi @HaoK, 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; |
@@ -90,7 +90,7 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) | |||
AuthorizationEndpoint = GoogleDefaults.AuthorizationEndpoint, | |||
TokenEndpoint = GoogleDefaults.TokenEndpoint, | |||
Scope = { "openid", "profile", "email" }, | |||
SaveTokensAsClaims = true | |||
SaveTokensInAuthenticationProperties = true |
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.
SaveTokens. We can say where in the comments.
What, no usage sample? |
{ | ||
identity.AddClaim(new Claim("access_token", tokens.AccessToken, | ||
ClaimValueTypes.String, Options.ClaimsIssuer)); | ||
var toks = new List<AuthenticationToken>(); |
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.
sp, tokens.
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.
At least this way the tokens are grouped together.
Well the usage without identity is going to be pretty gross, so I didn't update the samples, it'd basically be this:
We definitely need to add some sugar around extracting an individual token |
Please update the sample and propose some sugar. "This is Hard" is one of the main complaints we're trying to address. |
Sure, but the difference in the fix is whether its identity that makes it easier or security. The feedback from the last PR I thought was that the token APIs belong in identity. I don't think we should try to fix this at the security layer. I think the basic pieces to build the sugar should be exposed in Security, but its identity that makes the end to end nice. |
|
||
private static string TokenNamesKey = ".TokenNames"; | ||
|
||
public static void StoreTokens(AuthenticationProperties properties, IEnumerable<AuthenticationToken> tokens) |
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's a little odd to have these as static here. I'd expect them to be extension 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.
Yeah could go either way here, I left them as helpers for now, extension methods work just as well...
Yes Identity needs to provide support for some of the larger end-to-end scenarios (multiple providers, local account, etc.), but we also need a usable solution that does not require Identity for the basic scenarios (single provider). |
So the whole remote authentication flow is non trivial, but I can certainly add sugar to enable something like this:
The one bit of weirdness is that the authentication scheme passed in needs to be the SignInAsScheme and not the actual scheme since remote auth schemes do not support Authenticate anymore... |
Updated the social sample with usage, I can flip the helpers on AuthenticationToken to extension methods if we want, they do feel a bit clunky this way. But you get the idea anyways |
Most of our customers don't and won't use identity. So design the api without identity in mind. |
The current direction in this iteration of the PRs is that the AuthN layer exposes a few 'nicer' apis that easily let you extract/store named tokens from AuthenticationProperties. Its up to the caller to manage these/associate these with users. Identity will do this, but if you aren't using identity, you or your customers will need to associate/manage the tokens somehow on your own |
Basically this PR just exposes a simpler way to get a token out of facebook/google, via. |
await context.Response.WriteAsync("Access Token: " + await AuthenticationToken.GetTokenAsync(context, CookieAuthenticationDefaults.AuthenticationScheme, "access_token") + "<br>"); | ||
await context.Response.WriteAsync("Refresh Token: " + await AuthenticationToken.GetTokenAsync(context, CookieAuthenticationDefaults.AuthenticationScheme, "refresh_token") + "<br>"); | ||
await context.Response.WriteAsync("Token Type: " + await AuthenticationToken.GetTokenAsync(context, CookieAuthenticationDefaults.AuthenticationScheme, "token_type") + "<br>"); | ||
await context.Response.WriteAsync("expires_at: " + await AuthenticationToken.GetTokenAsync(context, CookieAuthenticationDefaults.AuthenticationScheme, "expires_at") + "<br>"); |
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.
And the flow if you need to update any tokens is as follows?
AuthenticateAsync(SignInScheme) to get the ClaimsPrincipal and AuthProperties,
Read the tokens from AuthProperties
Refresh the tokens
Store the tokens back in AuthProperties (or create a new instance?)
Call SignIn(SignInScheme) to persist the changes?
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.
How about context.Authentication.GetTokenAsync("access_token")
that used Automatic by default? That would avoid the weirdness of specifying signin-scheme.
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.
Updating the tokens that way seems pretty awkward. But I guess so at this layer.
Sure I can add overloads with the automatic scheme, that does make things a lot nicer.
Updated with new extension methods, and AutomaticScheme overload which simplifies the calling code a bit |
} | ||
|
||
|
||
public static async Task<string> GetTokenAsync(this AuthenticationManager manager, string signedInScheme, string tokenName) |
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's just called signInScheme
, not signedInScheme
.
⌚ Curious what others think, and what it looks like in Identity. |
This seems like a basic enough abstraction. |
I like this approach, though it requires too much plumbing to my taste 😄 IMHO, updating var ticket = context.Authentication.AuthenticateAsync("Cookies");
var token = ticket?.Properties?.Items?["access_token"]; We could also provide a /// <summary>
/// Gets a given property from the authentication ticket.
/// </summary>
/// <param name="ticket">The authentication ticket.</param>
/// <param name="property">The specific property to look for.</param>
/// <returns>The value corresponding to the property, or <c>null</c> if the property cannot be found.</returns>
public static string GetProperty(this AuthenticationTicket ticket, string property) {
if (ticket == null) {
throw new ArgumentNullException(nameof(ticket));
}
string value;
if (!ticket.Properties.Items.TryGetValue(property, out value)) {
return null;
}
return value;
} var ticket = context.Authentication.AuthenticateAsync("Cookies");
var token = ticket?.GetProperty("access_token"); |
@@ -781,6 +789,13 @@ private static TestServer CreateServer(GoogleOptions options, Func<HttpContext, | |||
{ | |||
await context.Authentication.ChallengeAsync("Google"); | |||
} | |||
else if (req.Path == new PathString("/tokens")) | |||
{ | |||
var authContext = new AuthenticateContext(TestExtensions.CookieAuthenticationScheme); |
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.
🔫
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 gun for? I can't nuke the scheme because its a required argument, I could switch it to automatic scheme but that's just less clear I think
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.
The gun is for me, this syntax is literally killing me (though it's absolutely not specific to this PR).
Le 17 févr. 2016 à 21:44, Hao Kung notifications@github.com a écrit :
In test/Microsoft.AspNetCore.Authentication.Test/Google/GoogleMiddlewareTests.cs:
@@ -781,6 +789,13 @@ private static TestServer CreateServer(GoogleOptions options, Func<HttpContext,
{
await context.Authentication.ChallengeAsync("Google");
}
else if (req.Path == new PathString("/tokens"))
{
What's the gun for? I can't nuke the scheme because its a required argument, I could switch it to automatic scheme but that's just less clear I thinkvar authContext = new AuthenticateContext(TestExtensions.CookieAuthenticationScheme);
—
Reply to this email directly or view it on GitHub.
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.
Ah okay... Yeah its clunky yes, this is the raw no sugar 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.
Internally we have a fairly widespread pattern of creating a context, and passing it to a method, I don't think its THAT bad. But its not the best public API no :)
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, I'm totally fine using this pattern with protected members you override (it's not really different from calling a method on event handler args) but I think it's a terrible pattern for public APIs, as it's counterintuitive: instantiating a context object and expect the method you call to alter it is rather unusual.
Le 17 févr. 2016 à 21:55, Hao Kung notifications@github.com a écrit :
In test/Microsoft.AspNetCore.Authentication.Test/Google/GoogleMiddlewareTests.cs:
@@ -781,6 +789,13 @@ private static TestServer CreateServer(GoogleOptions options, Func<HttpContext,
{
await context.Authentication.ChallengeAsync("Google");
}
else if (req.Path == new PathString("/tokens"))
{
Internally we have a fairly widespread pattern of creating a context, and passing it to a method, I don't think its THAT bad. But its not the best public API no :)var authContext = new AuthenticateContext(TestExtensions.CookieAuthenticationScheme);
—
Reply to this email directly or view it on GitHub.
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 agree, basically its the internal guts exposed, which is why it feels a bit gross...
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.
But this actually IS the public API between the lower HttpAbstractions layer and IAuthenticationHandlers :) Its just usually sugar'd over by the AuthenticationManager. I think the reason why we felt this API didn't need sugar is because the developers who are using AuthenticationProperties are typically the ones who would be familiar enough with this pattern.
I don't think AuthenticationProperties is really used much except for shuttling data around (identity uses it a bit, and now we stick tokens there), but in general it should be hidden. That said, go ahead and file a new issue to discuss this if you want, but I don't want rip open any lower layers as part of this change if I don't have to. |
@HaoK sounds like there are no major objections so far. Want to try filling in the other providers, and give Jwt a try while you're at it? |
-Updated the rest of the middleware |
/// This property is set to <c>false</c> by default to reduce | ||
/// the size of the final authentication cookie. | ||
/// </summary> | ||
public bool SaveTokens { get; set; } |
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.
Hum, I don't think this property is generic enough to be in the AuthenticationOptions
base class.
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.
Agree
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.
This could be true by default for Bearer as it wouldn't have the cookie size concerns.
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 the only one that doesn't use this now is Cookies... And there's nothing preventing that from saving tokens either...
Its basically a flag that tells the auth middleware to save tokens into AuthenticationProperties if it happens to have any tokens. If JwtBearer can use this flag, I don't see why its not generically applicable. AuthenticationMiddleware who don't have any tokens can just ignore this option...
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.
RemoteAuth + Bearer, right?
Not Cookies, not Basic, not cert auth, not Windows, 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.
Made it true by default for JwtBearer,
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.
Okay, moved SaveTokens back to RemoteAuth and added a new SaveToken which defaults to True for JwtBearer
👍 |
Identity PR here: aspnet/Identity#748 |
@Tratcher want to take a final look over this before I merge? |
var expiresAt = Options.SystemClock.UtcNow + TimeSpan.FromSeconds(value); | ||
authTokens.Add(new AuthenticationToken | ||
{ | ||
Name = "expires_at", |
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, no plan to make it a property on AuthenticationToken
?
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.
Doesn't sound like you've convinced @Tratcher and it definitely makes things more complicated, so my default is no for now as well, you can file an issue to consider adding it later
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.
Okay, will do.
@HaoK don't forget to update HandleSignOutAsync: https://github.com/aspnet/Security/blob/haok/2-16tok/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs#L108-L109. |
Thanks @PinpointTownes oh how I love lack of test coverage :) |
{ | ||
ticket.Properties.StoreTokens(new[] | ||
{ | ||
new AuthenticationToken { Name = "Bearer", Value = token } |
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.
Should we name it access_token
for consistency?
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 have enough context to say, I would say we should name it 'correctly' in the context of JwtBearer as opposed to consistently. I think it's better for it to be as clear as possible what this token is based on the name. That said, if access_token is correct-ish, consistency is great :)
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.
Yep, it's correct (it's the official name used by the specs: https://tools.ietf.org/html/rfc6750#section-2.3)
|
No description provided.