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

Save tokens in auth properties instead of claims #698

Closed
wants to merge 9 commits into from
Closed

Conversation

HaoK
Copy link
Member

@HaoK HaoK commented Feb 16, 2016

No description provided.

@dnfclas
Copy link

dnfclas commented Feb 16, 2016

Hi @HaoK, 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;

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

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.

@Tratcher
Copy link
Member

What, no usage sample?

{
identity.AddClaim(new Claim("access_token", tokens.AccessToken,
ClaimValueTypes.String, Options.ClaimsIssuer));
var toks = new List<AuthenticationToken>();
Copy link
Member

Choose a reason for hiding this comment

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

sp, tokens.

Copy link
Member

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.

@HaoK
Copy link
Member Author

HaoK commented Feb 16, 2016

Well the usage without identity is going to be pretty gross, so I didn't update the samples, it'd basically be this:

                            var authContext = new AuthenticateContext("<the sign in as scheme">);
                            await context.Authentication.AuthenticateAsync(authContext);
                            var tokens = AuthenticationToken.GetTokens(new AuthenticationProperties(authContext.Properties));

We definitely need to add some sugar around extracting an individual token

@Tratcher
Copy link
Member

Please update the sample and propose some sugar. "This is Hard" is one of the main complaints we're trying to address.

@HaoK
Copy link
Member Author

HaoK commented Feb 16, 2016

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

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.

Copy link
Member Author

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

@Tratcher
Copy link
Member

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

@HaoK
Copy link
Member Author

HaoK commented Feb 16, 2016

So the whole remote authentication flow is non trivial, but I can certainly add sugar to enable something like this:

   httpContext.Authentication.GetToken("Cookies", "access_token")

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

@HaoK
Copy link
Member Author

HaoK commented Feb 16, 2016

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

@leastprivilege
Copy link
Contributor

Most of our customers don't and won't use identity. So design the api without identity in mind.

@HaoK
Copy link
Member Author

HaoK commented Feb 16, 2016

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

@HaoK
Copy link
Member Author

HaoK commented Feb 16, 2016

Basically this PR just exposes a simpler way to get a token out of facebook/google, via.
AuthenticationToken.GetToken(context, "Cookie", "access_token") or context.Authentication.GetToken("Cookie", "access_token"). What you do with this token isn't really addressed

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

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?

Copy link
Member

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.

Copy link
Member Author

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.

@HaoK
Copy link
Member Author

HaoK commented Feb 16, 2016

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

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.

@Tratcher
Copy link
Member

⌚ Curious what others think, and what it looks like in Identity.

@leastprivilege
Copy link
Contributor

This seems like a basic enough abstraction.

@kevinchalet
Copy link
Contributor

I like this approach, though it requires too much plumbing to my taste 😄

IMHO, updating AuthenticationManager.AuthenticateAsync to simply return an AuthenticationTicket would be much simpler (the current signature is ABSOLUTELY insane anyway).

var ticket = context.Authentication.AuthenticateAsync("Cookies");
var token = ticket?.Properties?.Items?["access_token"];

We could also provide a GetProperty helper to make the properties easier to extract. Here's the one used by ASOS:

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

Choose a reason for hiding this comment

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

🔫

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

Copy link
Contributor

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"))
    
  •                    {
    
  •                        var authContext = new AuthenticateContext(TestExtensions.CookieAuthenticationScheme);
    
    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


Reply to this email directly or view it on GitHub.

Copy link
Member Author

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

Copy link
Member Author

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

Copy link
Contributor

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"))
    
  •                    {
    
  •                        var authContext = new AuthenticateContext(TestExtensions.CookieAuthenticationScheme);
    
    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 :)


Reply to this email directly or view it on GitHub.

Copy link
Member Author

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

Copy link
Member Author

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.

@HaoK
Copy link
Member Author

HaoK commented Feb 17, 2016

updating AuthenticationManager.AuthenticateAsync to simply return an AuthenticationTicket would be much simpler (the current signature is ABSOLUTELY insane anyway).

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.

@Tratcher
Copy link
Member

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

@HaoK
Copy link
Member Author

HaoK commented Feb 18, 2016

-Updated the rest of the middleware
-store JwtBearer token as Bearer
-StoreTokens deletes previous tokens, so last store wins
-Added tests

/// 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; }
Copy link
Contributor

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.

Copy link
Member

Choose a reason for hiding this comment

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

Agree

Copy link
Member

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.

Copy link
Member Author

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

Copy link
Member

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

Copy link
Member Author

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,

Copy link
Member Author

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

@Tratcher
Copy link
Member

👍
How's it looking in Identity?

@HaoK
Copy link
Member Author

HaoK commented Feb 18, 2016

Identity PR here: aspnet/Identity#748

@HaoK
Copy link
Member Author

HaoK commented Feb 23, 2016

@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",
Copy link
Contributor

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?

Copy link
Member Author

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, will do.

@kevinchalet
Copy link
Contributor

@HaoK
Copy link
Member Author

HaoK commented Feb 23, 2016

Thanks @PinpointTownes oh how I love lack of test coverage :)

{
ticket.Properties.StoreTokens(new[]
{
new AuthenticationToken { Name = "Bearer", Value = token }
Copy link
Contributor

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?

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

Copy link
Contributor

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)

@Tratcher
Copy link
Member

Tratcher commented Mar 3, 2016

:shipit:

@HaoK
Copy link
Member Author

HaoK commented Mar 3, 2016

ace166f

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