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

Iteration 4 - Using Auth 2.0/Options 2.0 packages #1170

Closed
wants to merge 25 commits into from
Closed

Conversation

HaoK
Copy link
Member

@HaoK HaoK commented Mar 31, 2017

Previous PR: #1151

@Tratcher clean PR to start with, rebased and using the Http Abstractions/Options 2.0 interfaces

@dnfclas
Copy link

dnfclas commented Mar 31, 2017

@HaoK,
Thanks for having already signed the Contribution License Agreement. Your agreement was validated by .NET Foundation. We will now review your pull request.
Thanks,
.NET Foundation Pull Request Bot

throw new InvalidOperationException("The MetadataAddress or Authority must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false.");
}

Options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(Options.MetadataAddress, new OpenIdConnectConfigurationRetriever(),
Copy link
Contributor

Choose a reason for hiding this comment

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

@HaoK hum, I thought all the initialization stuff was supposed to be done in the options?
'cause AFAICT, this iteration still suffers from the same concurrency issues as the previous PRs 😅

Copy link
Member Author

Choose a reason for hiding this comment

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

Whoops, I forgot we needed validation/fixup in options for that reason, well good thing you reminded me why I need to push for that feature :)

@HaoK HaoK mentioned this pull request Apr 13, 2017
13 tasks
@HaoK
Copy link
Member Author

HaoK commented Apr 13, 2017

Updated PR with some refactoring/cleaning up around Options/Events initialization.

@Tratcher I think this has everything we need for the initial iteration (other than the AddXyz() empty overloads that use Config from DI, I only added that to Google so far)

/// <summary>
/// Used to ensure that the options are only initialized once.
/// </summary>
public bool Initialized { 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.

Yurk 😲

Is there a particular reason this initialization logic is not something handled directly by the options stack?

Copy link
Member Author

Choose a reason for hiding this comment

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

Well auth is doing something a bit different than normal options usages, as handlers are doing fixup after all the Configures have been run. If we bring back some kind of lifetime/second validation pass we could do it in options directly, but for now, we have to roll our own...

Copy link
Contributor

@kevinchalet kevinchalet Apr 13, 2017

Choose a reason for hiding this comment

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

I guess it would be cleaner (and probably safer?) if options was responsible of the entire initialization process, 'cause that's likely the most appropriate place to determine whether options are singleton (= initialization should happen only once) or scoped (= initialization should happen for every instance).

Copy link
Contributor

Choose a reason for hiding this comment

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

I took a look at the options changes, and there's indeed nothing (yet) that could help with that. Have you considered introducing an IOptionsValidator<T>/IOptionsInitializerT> that would be run after all the IConfigureOptions<T>? (or you could bring back ordered setups).

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 Validator/Intializer was there in the origional options PR but cut due to not wanting to rush that feature into options, we still have time to revisit where responsibility for single intialization goes before we ship, but for this initial checkin, its going to live in security

/// <summary>
/// Used to prevent concurrent access during intialization.
/// </summary>
public object InitalizeLock { get; } = new object();
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: typo 😄

Copy link
Member Author

Choose a reason for hiding this comment

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

Doh, fixed

Security.sln Outdated
@@ -54,286 +52,436 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIdConnect.AzureAdSample
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test", "test\Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test\Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test.csproj", "{51563775-C659-4907-9BAF-9995BAB87D01}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication", "src\Microsoft.AspNetCore.Authentication\Microsoft.AspNetCore.Authentication.csproj", "{BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Options", "..\Options\src\Microsoft.Extensions.Options\Microsoft.Extensions.Options.csproj", "{151B7C1C-8E47-48CC-B04C-7414001D0F7B}"
Copy link
Member

Choose a reason for hiding this comment

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

isn't the options work done? What did you tweak from dataprotection?

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 forgot to push the updated sln, doing that now

if (user?.Identity?.IsAuthenticated ?? false)
{
await next();
}
else
{
// We can do this because of options.AutomaticChallenge = true;
await context.Authentication.ChallengeAsync();
await context.ChallengeAsync(JwtBearerDefaults.AuthenticationScheme);
Copy link
Member

Choose a reason for hiding this comment

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

ChallengeAsync()

services.AddAuthentication(sharedOptions =>
sharedOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme);
sharedOptions.DefaultAuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme);
Copy link
Member

Choose a reason for hiding this comment

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

also DefaultSignInScheme and OIDC as the DefaultChallengeScheme.

sharedOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme);
{
sharedOptions.DefaultAuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
Copy link
Member

Choose a reason for hiding this comment

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

OIDC as DefaultChallengeScheme

@@ -158,7 +164,7 @@ await context.Authentication.SignOutAsync(OpenIdConnectDefaults.AuthenticationSc
{
// This is what [Authorize] calls
// The cookie middleware will intercept this 401 and redirect to /login
await context.Authentication.ChallengeAsync();
await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme);
Copy link
Member

Choose a reason for hiding this comment

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

ChallengeAsync()
This comment is wrong, this sample has no /login. It may have been copied from SocialAuth. This sample should challenge OIDC implicitly.


// Deny anonymous request beyond this point.
if (user == null || !user.Identities.Any(identity => identity.IsAuthenticated))
{
// This is what [Authorize] calls
// The cookie middleware will intercept this 401 and redirect to /login
await context.Authentication.ChallengeAsync();
await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme);
Copy link
Member

Choose a reason for hiding this comment

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

ChallengeAsync()

await context.Response.WriteAsync("Token Type: " + await context.Authentication.GetTokenAsync("token_type") + "<br>");
await context.Response.WriteAsync("expires_at: " + await context.Authentication.GetTokenAsync("expires_at") + "<br>");

await context.Response.WriteAsync("Access Token: " + await context.GetTokenAsync(CookieAuthenticationDefaults.AuthenticationScheme, "access_token") + "<br>");
Copy link
Member

Choose a reason for hiding this comment

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

Why do you need the auth scheme here? Can't it use DefaultAuthScheme?

Copy link
Member Author

Choose a reason for hiding this comment

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

That overload is gone right now, could add it back if we really want.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, this is clunky

@@ -2474,7 +2474,7 @@
},
{
"Kind": "Method",
"Name": "SkipToNextMiddleware",
"Name": "StopProcessing",
Copy link
Member

Choose a reason for hiding this comment

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

revert the baseline files, disable APICheck, and file a followup bug

/// </summary>
/// <typeparam name="TOptions">Specifies which type for of AuthenticationOptions property</typeparam>
public abstract class AuthenticationHandler<TOptions> : IAuthenticationHandler where TOptions : AuthenticationOptions
namespace Microsoft.AspNetCore.Authentication {
Copy link
Member

Choose a reason for hiding this comment

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

nit: new lines


protected UrlEncoder UrlEncoder { get; private set; }

public IAuthenticationHandler PriorHandler { get; set; }
Copy link
Member

Choose a reason for hiding this comment

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

Should we leave all of the public/protected properties as Obsolete Error with the fwlink?

Copy link
Member

Choose a reason for hiding this comment

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

Easier to diff if you revert the order change

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 think its worth preserving things in the Handler just for obsolete, since they will see the error already from all of the UseXyzs

Copy link
Member

Choose a reason for hiding this comment

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

Those are different audiences. These would be for implementors, not consumers.

@HaoK
Copy link
Member Author

HaoK commented Apr 14, 2017

Updated samples feedback, I'm not sure I love DefaultAuthenticationScheme, I thought I had named it DefaultAuthenticateScheme, which maps more closely to what it means... thoughts?

@HaoK
Copy link
Member Author

HaoK commented Apr 14, 2017

Ah I see what happened, its GetDefaultAuthenticateSchemeAsync in the real interface IAuthenticationSchemeProvider, I just forgot to rename the property on the options. So we need to be consistent one way or the other. I'll submit a PR in HTTP

var user = context.User;

// This is what [Authorize] calls
// var user = await context.AuthenticateAsync(AuthenticationManager.AutomaticScheme);
// var user = await context.AuthenticateAsync(DefaultAuthenticateScheme);
Copy link
Member

Choose a reason for hiding this comment

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

Is there AuthenticateAsync() that resolves the default from the service?

var user = context.User;

// This is what [Authorize] calls
// var user = await context.Authentication.AuthenticateAsync(AuthenticationManager.AutomaticScheme);
// var user = await context.AuthenticateAsync(AuthenticationManager.AutomaticScheme);
Copy link
Member

Choose a reason for hiding this comment

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

AutomaticScheme.
AuthenticateAsync()

Security.sln Outdated
@@ -54,286 +52,382 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIdConnect.AzureAdSample
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test", "test\Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test\Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test.csproj", "{51563775-C659-4907-9BAF-9995BAB87D01}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication", "src\Microsoft.AspNetCore.Authentication\Microsoft.AspNetCore.Authentication.csproj", "{BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}"
Copy link
Member

Choose a reason for hiding this comment

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

revert

};
});

services.AddAuthentication(sharedOptions =>
Copy link
Member

Choose a reason for hiding this comment

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

Let's put these defaults at the top of each sample so they don't get lost under all of the options noise.


public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory)
{
loggerfactory.AddConsole(Microsoft.Extensions.Logging.LogLevel.Information);
Copy link
Member

Choose a reason for hiding this comment

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

namespace?

Copy link
Member Author

Choose a reason for hiding this comment

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

Its because Azure also has a log level so this is disambiguiating

});


services.AddAuthentication(sharedOptions =>
Copy link
Member

Choose a reason for hiding this comment

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

Move defaults to top

@@ -144,32 +130,31 @@ await context.Authentication.SignOutAsync(OpenIdConnectDefaults.AuthenticationSc
return;
}

// CookieAuthenticationOptions.AutomaticAuthenticate = true (default) causes User to be set
// DefaultAuthenticationScheme causes User to be set
Copy link
Member

Choose a reason for hiding this comment

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

DefaultAuthenticateScheme

var user = context.User;

// This is what [Authorize] calls
// var user = await context.Authentication.AuthenticateAsync(AuthenticationManager.AutomaticScheme);
// var user = await context.AuthenticateAsync(DefaultAuthenticateScheme);
Copy link
Member

Choose a reason for hiding this comment

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

AuthenticateAsync()

await context.Response.WriteAsync("Token Type: " + await context.Authentication.GetTokenAsync("token_type") + "<br>");
await context.Response.WriteAsync("expires_at: " + await context.Authentication.GetTokenAsync("expires_at") + "<br>");

await context.Response.WriteAsync("Access Token: " + await context.GetTokenAsync(CookieAuthenticationDefaults.AuthenticationScheme, "access_token") + "<br>");
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, this is clunky


services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultAuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme;
Copy link
Member

Choose a reason for hiding this comment

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

DefaultAuthenticateScheme

{
if (!ChallengeCalled && Options.AutomaticChallenge && Response.StatusCode == 401)
Events = Options.Events;
if (Options.EventsType != null)
Copy link
Member

Choose a reason for hiding this comment

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

While it's strange for someone to set both EventsType and Events, it's even stranger that EventsType is preferred.

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 we have to do this because we always new up the events today.

Copy link
Member

Choose a reason for hiding this comment

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

I just meant we should change it to if (Events == null && Options.EventsType != null)

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 so Events is always not null in general, as the default is an empty events object, if we added that check, they'd have to explicitly null out events and set eventstype.

var handler = (AuthenticationHandler<TOptions>)state;
await handler.FinishResponseOnce();
}
// REVIEW: should we validate options only once inside of the lock?
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 it needs to be outside. If the options are wrong then you need to fail every request, not just the first one.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fair enough, I'll nuke the review


if (ShouldHandleScheme(AuthenticationManager.AutomaticScheme, Options.AutomaticAuthenticate))
Options = OptionsSnapshot.Get(Scheme.Name) ?? new TOptions();
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 cookie auth is the only one that works with default options. We'd be better off throwing a specific error here and requiring providers to register their options.

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 its pretty much an error in any implementation of IOptionsSnapshot for Get to return null, so we could actually just assume that it always returns not null.

}
}
/// <returns>A new instance of the events instance.</returns>
protected virtual Task<object> CreateEventsAsync() => Task.FromResult<object>(null);
Copy link
Member

Choose a reason for hiding this comment

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

Not null, return a new object or NoEventsDefined

}

public async Task AuthenticateAsync(AuthenticateContext context)
public async Task<AuthenticateResult> AuthenticateAsync(AuthenticateContext context)
Copy link
Member

Choose a reason for hiding this comment

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

Context is not used? It's not even null checked.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sweet, means we can nuke the type then

}

_next = next;
_next = next ?? throw new ArgumentNullException(nameof(next));
Copy link
Member

Choose a reason for hiding this comment

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

syntax


public async Task Invoke(HttpContext context)
{
var handler = CreateHandler();
await handler.InitializeAsync(Options, context, Logger, UrlEncoder);
var oldFeature = context.Features.Get<IAuthenticationFeature>();
Copy link
Member

Choose a reason for hiding this comment

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

Why are you expecting there to be an old feature? There can only be one auth middleware now, correct?

Copy link
Member Author

Choose a reason for hiding this comment

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

"Should be one" as opposed to 'can only be one'. Really we just need at least one. But I'm fine with dropping the save/restore goop

Copy link
Member Author

Choose a reason for hiding this comment

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

Is it important to null out features after we are done? Or can I nuke the try/finally entirely?

// REVIEW: alternatively could depend on a routing middleware to do this

// Give any IAuthenticationRequestHandler schemes a chance to handle the request
var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
Copy link
Member

Choose a reason for hiding this comment

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

Move HandleRequestAsync processing above defaultAuthenticate. HandleRequestAsync may short circuit the request.

Copy link
Member Author

Choose a reason for hiding this comment

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

Moved


await _next(context);
}
finally
Copy link
Member

Choose a reason for hiding this comment

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

Yes, you can remove the finally and the nulling out.

await Options.Events.MessageReceived(messageReceivedContext);
if (messageReceivedContext.CheckEventResult(out result))
await Events.MessageReceived(messageReceivedContext);
if (messageReceivedContext.IsProcessingComplete(out result))
Copy link
Member

@Tratcher Tratcher Apr 17, 2017

Choose a reason for hiding this comment

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

Still not a verb... IsFoo is used for bool properties. This might be good enough though...

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 this is internal plumbing right? This typically isn't called by event handlers is it?

@@ -12,9 +12,9 @@ namespace Microsoft.AspNetCore.Builder
public static class AuthAppBuilderExtensions
{
/// <summary>
/// Adds the <see cref="AuthenticationMiddleware"/> middleware to the specified <see cref="IApplicationBuilder"/>, which enables authentication capabilities.
/// Adds the <see cref="AuthenticationMiddleware"/> handler to the specified <see cref="IApplicationBuilder"/>, which enables authentication capabilities.
Copy link
Member

Choose a reason for hiding this comment

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

One too many replacements...

@HaoK
Copy link
Member Author

HaoK commented Apr 17, 2017

Okay I think everything major has been addressed here, MVC/Identity are pretty much done as well. Working on MusicStore next.


public static IServiceCollection AddFacebookAuthentication(this IServiceCollection services, string authenticationScheme, Action<FacebookOptions> configureOptions)
{
services.AddSingleton<IConfigureOptions<FacebookOptions>, FacebookConfigureOptions>();
Copy link
Contributor

Choose a reason for hiding this comment

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

So, the magical IConfiguration-based binding will be used even if you specify a configuration delegate? Not sure I'm a huge fan of that... specially since this binder doesn't seem foolproof 😄

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, your delegate runs after the binding so it 'wins'. Consider the AddXyz stuff early preview stuff we want people to try and give feedback on, the behavior will almost certainly evolve from here. Also in general the binding will be against an empty configuration section so its basically a default no-op (unless you happen to actually define a matching section to opt into the behavior)

Copy link
Contributor

Choose a reason for hiding this comment

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

Well, your delegate runs after the binding so it 'wins'.

Only if the DI container respects the services registration order, right? (IConfigureOptions.Order is no longer a thing in 1.0/2.0, so it entirely relies on the DI capabilities)

It would be way clearer if this feature was more explicit (e.g by flowing the IConfiguration as a parameter). It would also the developer to select the nodes he/she wants:

services.AddFacebookAuthentication(Configuration.GetSection("security:authentication:facebook"));

Copy link
Member Author

Choose a reason for hiding this comment

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

That already exists, if you want to bind against a section, you can just call Configure(Configuration.GetSection("facebook")).

DI respecting registration order is an implicit contract we already have today with options, especially without order, we must use registration order as order...

Copy link
Contributor

Choose a reason for hiding this comment

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

That already exists, if you want to bind against a section, you can just call

I know. My point is that the automatic configuration import should be explicit. If I provide a delegate, I don't expect the system to combine it with another source, specially since it's not consistent (cookies and OAuth2 don't use it)

DI respecting registration order is an implicit contract we already have today with options, especially without order, we must use registration order as order...

A requirement that was not clear at all in 1.0 and caused much much pain when using custom containers (aspnet/Hosting#785 / aspnet/DependencyInjection#433). But okay, point taken.

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 so the convention totally doesn't work when its not a 'default' section, which is why I didn't add it for cookies. If multiple OIDC handlers are going to be used frequently, it doesn't make a good candidate either.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree that might be a potential problem with no way to turn off the binding.

Not just a potential problem: it's actually completely broken when you have multiple schemes of the same type (e.g multiple OIDC or JWT middleware), because the options system has no way to tell that JwtBearerConfigureOptions registered with the first AddJwtBearerAuthentication() call should go with the scheme A and not the scheme B (all the JwtBearerConfigureOptions apply to all the registered schemes).

To fix that, you'd have to introduce "named configure options".

Copy link
Member Author

Choose a reason for hiding this comment

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

Well the named configure options already exist, but this 'default' binding section really won't work with multiple schemes of the same type, that's why I didn't push it everywhere, its makes the most sense for singleton frameworks like identity/MVC.

Its possible that this won't work well for auth at all if all of the schemes are likely to have multiple instances of the same type.

Copy link
Member Author

Choose a reason for hiding this comment

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

For the first iteration I'll just decouple the AddXyz() from the AddXyz(o => { }) again for now, we can figure out how to make things consistent later

Copy link
Contributor

Choose a reason for hiding this comment

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

@HaoK yeah, sounds better 👍


public override async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
await base.InitializeAsync(scheme, context);
Copy link
Contributor

Choose a reason for hiding this comment

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

The cool thing with the previous design is that we didn't have to call any of the base methods in the handler stuff. Any particular not to reason to preserve this pattern in 2.0?

Copy link
Member Author

Choose a reason for hiding this comment

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

You mean if you override Initialize and don't call base?

Copy link
Member Author

@HaoK HaoK Apr 18, 2017

Choose a reason for hiding this comment

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

Or are you just asking for us to make it impossible to forget to call base via a separate virtual for children to implement.

InitializeAsync(scheme, context) => // do stuff, then call protected virtual InitializeAsync() { }

Copy link
Contributor

Choose a reason for hiding this comment

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

You mean if you override Initialize and don't call base?

Yep. This way, the chances of messing with the default initialization logic are low if you don't forget to call the base method.

Copy link
Member Author

Choose a reason for hiding this comment

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

Initialize on the other hand, can be refactored a bit, with a new parameterless protected virtual InitializeHandler() that's called after Options/Events have all been initialized.


protected override void InitializeOptions()
{
base.InitializeOptions();
Copy link
Contributor

Choose a reason for hiding this comment

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

(same remark)

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'm not sure we can do too much to avoid requiring the call to base, because we do have a chain in the base classes today (Base => Remote => OAuth, etc), other than adding another virtual InitializeYourOptions() which is really the same as this one, but less likely to clobber :)

@Tratcher
Copy link
Member

Looks like there's still several smaller comments un-addressed, but otherwise you're ready to go.

@@ -16,14 +16,16 @@ public static class OpenIdConnectExtensions
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddOpenIdConnectAuthentication(this IServiceCollection services)
=> services.AddOpenIdConnectAuthentication(OpenIdConnectDefaults.AuthenticationScheme, _ => { });
{
services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, OpenIdConnectConfigureOptions>();
Copy link
Contributor

Choose a reason for hiding this comment

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

Wait, if you call this method to register an OIDC provider using IConfiguration + one of the other overloads to register another provider using a configuration delegate, this "configure options" instance will also apply to the other instances, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, these only configure the 'default' instance today, so the one named "OpenIdConnect" or whatever the default scheme name for each auth handler.

 public OpenIdConnectConfigureOptions(IConfiguration config) :
            base(OpenIdConnectDefaults.AuthenticationScheme,
                options => config.GetSection(OpenIdConnectDefaults.AuthenticationScheme).Bind(options))
        { }
``

Copy link
Member Author

Choose a reason for hiding this comment

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

The only way to apply a configure to all named instances is to use ConfigureAll/specify null name, the overloads that don't take name, use the empty string which means the 'default' instance.

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 for auth we always have the scheme name, so we can always just use that.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, cool then. Other question: what happens if I call AddOpenIdConnectAuthentication() twice? (which is likely not a good idea). Will the "configure options" be run twice? Wouldn't using TryAddSingleton make sense here?

Copy link
Member Author

@HaoK HaoK Apr 18, 2017

Choose a reason for hiding this comment

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

TryAddSingleton isn't appropriate for options since they are meant to be layered on, for example that means if you Configure before you call Add, the bind wouldn't get added, which would be super weird.

Binding twice isn't wrong, just a bit weird, and you'd have weirdness like, Bind, Configure, Bind would likely stomp all over stuff since the binder is a bit eager (and dumb)

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay. Anyway, this shouldn't be a problem, since the second call to AddOpenIdConnectAuthentication() should throw immediately.

@HaoK
Copy link
Member Author

HaoK commented Apr 18, 2017

Okay I think that's the last change I have planned for this iteration (other than anything we will need from the other repos), I made the Initialize implementation non virtual, and have it call a new protected virtual InitializeHandler(), moved the two overrides we have today (Twitter/Cookies) to override that one instead, eliminating the need to call base in those situations.

@kevinchalet
Copy link
Contributor

@HaoK thanks for the great work (and for being so patient :trollface:)

Please ping me when consistent CI packages are available, I'd love to try them ASAP, to make sure everything I need is there.

@HaoK
Copy link
Member Author

HaoK commented Apr 18, 2017

Will do, thanks as always for all the great feedback @PinpointTownes :)

@HaoK
Copy link
Member Author

HaoK commented Apr 20, 2017

@PinpointTownes 24524 should have these changes now.

@HaoK HaoK closed this Apr 20, 2017
@kevinchalet
Copy link
Contributor

@HaoK thanks!

FWIW, I just finished porting ASOS to ASP.NET Core 2.0 and so far, the experience is not that bad (except all the duplicate abstraction types...!). It seems to work as expected and all the tests pass successfully.

I have a few remarks/suggestions/demands, but I'll open dedicated threads for them.

@HaoK HaoK deleted the haok/auth2 branch April 28, 2017 21:28
@HaoK HaoK restored the haok/auth2 branch April 28, 2017 21:28
@HaoK HaoK deleted the haok/auth2 branch August 7, 2017 16:59
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.

4 participants