-
Notifications
You must be signed in to change notification settings - Fork 599
Iteration 4 - Using Auth 2.0/Options 2.0 packages #1170
Conversation
@HaoK, |
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(), |
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.
@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 😅
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.
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 :)
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; } |
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.
Yurk 😲
Is there a particular reason this initialization logic is not something handled directly by the options stack?
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 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...
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 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).
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 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).
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 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(); |
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.
nit: typo 😄
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.
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}" |
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.
isn't the options work done? What did you tweak from dataprotection?
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 forgot to push the updated sln, doing that now
samples/JwtBearerSample/Startup.cs
Outdated
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); |
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.
ChallengeAsync()
services.AddAuthentication(sharedOptions => | ||
sharedOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme); | ||
sharedOptions.DefaultAuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme); |
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.
also DefaultSignInScheme and OIDC as the DefaultChallengeScheme.
sharedOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme); | ||
{ | ||
sharedOptions.DefaultAuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme; | ||
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; |
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.
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); |
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.
ChallengeAsync()
This comment is wrong, this sample has no /login
. It may have been copied from SocialAuth. This sample should challenge OIDC implicitly.
samples/SocialSample/Startup.cs
Outdated
|
||
// 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); |
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.
ChallengeAsync()
samples/SocialSample/Startup.cs
Outdated
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>"); |
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.
Why do you need the auth scheme here? Can't it use DefaultAuthScheme?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That overload is gone right now, could add it back if we really want.
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, this is clunky
@@ -2474,7 +2474,7 @@ | |||
}, | |||
{ | |||
"Kind": "Method", | |||
"Name": "SkipToNextMiddleware", | |||
"Name": "StopProcessing", |
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.
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 { |
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.
nit: new lines
|
||
protected UrlEncoder UrlEncoder { get; private set; } | ||
|
||
public IAuthenticationHandler PriorHandler { 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.
Should we leave all of the public/protected properties as Obsolete Error with the fwlink?
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.
Easier to diff if you revert the order change
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think its worth preserving things in the Handler just for obsolete, since they will see the error already from all of the UseXyzs
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.
Those are different audiences. These would be for implementors, not consumers.
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? |
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 |
samples/SocialSample/Startup.cs
Outdated
var user = context.User; | ||
|
||
// This is what [Authorize] calls | ||
// var user = await context.AuthenticateAsync(AuthenticationManager.AutomaticScheme); | ||
// var user = await context.AuthenticateAsync(DefaultAuthenticateScheme); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is 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); |
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.
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}" |
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.
revert
}; | ||
}); | ||
|
||
services.AddAuthentication(sharedOptions => |
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.
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); |
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.
namespace?
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.
Its because Azure also has a log level so this is disambiguiating
}); | ||
|
||
|
||
services.AddAuthentication(sharedOptions => |
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.
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 |
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.
DefaultAuthenticateScheme
samples/SocialSample/Startup.cs
Outdated
var user = context.User; | ||
|
||
// This is what [Authorize] calls | ||
// var user = await context.Authentication.AuthenticateAsync(AuthenticationManager.AutomaticScheme); | ||
// var user = await context.AuthenticateAsync(DefaultAuthenticateScheme); |
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.
AuthenticateAsync()
samples/SocialSample/Startup.cs
Outdated
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>"); |
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, this is clunky
|
||
services.AddAuthentication(sharedOptions => | ||
{ | ||
sharedOptions.DefaultAuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme; |
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.
DefaultAuthenticateScheme
{ | ||
if (!ChallengeCalled && Options.AutomaticChallenge && Response.StatusCode == 401) | ||
Events = Options.Events; | ||
if (Options.EventsType != null) |
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.
While it's strange for someone to set both EventsType and Events, it's even stranger that EventsType is preferred.
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 we have to do this because we always new up the events today.
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 just meant we should change it to if (Events == null && Options.EventsType != null)
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 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? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it needs to be outside. If the options are wrong then you need to fail every request, not just the first one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough, I'll nuke the review
|
||
if (ShouldHandleScheme(AuthenticationManager.AutomaticScheme, Options.AutomaticAuthenticate)) | ||
Options = OptionsSnapshot.Get(Scheme.Name) ?? new TOptions(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think 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.
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 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); |
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.
Not null, return a new object or NoEventsDefined
} | ||
|
||
public async Task AuthenticateAsync(AuthenticateContext context) | ||
public async Task<AuthenticateResult> AuthenticateAsync(AuthenticateContext context) |
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.
Context is not used? It's not even null checked.
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.
Sweet, means we can nuke the type then
} | ||
|
||
_next = next; | ||
_next = next ?? throw new ArgumentNullException(nameof(next)); |
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.
syntax
|
||
public async Task Invoke(HttpContext context) | ||
{ | ||
var handler = CreateHandler(); | ||
await handler.InitializeAsync(Options, context, Logger, UrlEncoder); | ||
var oldFeature = context.Features.Get<IAuthenticationFeature>(); |
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.
Why are you expecting there to be an old feature? There can only be one auth middleware now, correct?
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 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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is 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>(); |
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.
Move HandleRequestAsync processing above defaultAuthenticate. HandleRequestAsync may short circuit the request.
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.
Moved
|
||
await _next(context); | ||
} | ||
finally |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you 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)) |
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.
Still not a verb... IsFoo
is used for bool properties. This might be good enough though...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
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.
One too many replacements...
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>(); |
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 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 😄
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, 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)
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, 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"));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That 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...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That 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.
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 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.
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 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".
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 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.
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.
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
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.
@HaoK yeah, sounds better 👍
|
||
public override async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) | ||
{ | ||
await base.InitializeAsync(scheme, context); |
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 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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean if you override Initialize and don't call base?
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.
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() { }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You 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.
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.
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(); |
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.
(same remark)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure 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 :)
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>(); |
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.
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?
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.
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))
{ }
``
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 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.
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 for auth we always have the scheme name, so we can always just use that.
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, 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?
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.
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)
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. Anyway, this shouldn't be a problem, since the second call to AddOpenIdConnectAuthentication()
should throw immediately.
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. |
@HaoK thanks for the great work (and for being so patient ) Please ping me when consistent CI packages are available, I'd love to try them ASAP, to make sure everything I need is there. |
Will do, thanks as always for all the great feedback @PinpointTownes :) |
@PinpointTownes 24524 should have these changes now. |
@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. |
Previous PR: #1151
@Tratcher clean PR to start with, rebased and using the Http Abstractions/Options 2.0 interfaces