Repository navigation
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.
@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.
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.
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.
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.
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.
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.
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(); |
| 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.
isn't the options work done? What did you tweak from dataprotection?
There was a problem hiding this comment.
Yeah I forgot to push the updated sln, doing that now
| { | ||
| // We can do this because of options.AutomaticChallenge = true; | ||
| await context.Authentication.ChallengeAsync(); | ||
| await context.ChallengeAsync(JwtBearerDefaults.AuthenticationScheme); |
|
|
||
| services.AddAuthentication(sharedOptions => | ||
| sharedOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme); | ||
| sharedOptions.DefaultAuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme); |
There was a problem hiding this comment.
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.
OIDC as DefaultChallengeScheme
| // 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.
ChallengeAsync()
This comment is wrong, this sample has no /login. It may have been copied from SocialAuth. This sample should challenge OIDC implicitly.
| // 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); |
| 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.
Why do you need the auth scheme here? Can't it use DefaultAuthScheme?
There was a problem hiding this comment.
That overload is gone right now, could add it back if we really want.
| { | ||
| "Kind": "Method", | ||
| "Name": "SkipToNextMiddleware", | ||
| "Name": "StopProcessing", |
There was a problem hiding this comment.
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 { |
|
|
||
| protected UrlEncoder UrlEncoder { get; private set; } | ||
|
|
||
| public IAuthenticationHandler PriorHandler { get; set; } |
There was a problem hiding this comment.
Should we leave all of the public/protected properties as Obsolete Error with the fwlink?
There was a problem hiding this comment.
Easier to diff if you revert the order change
There was a problem hiding this comment.
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.
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 |
|
|
||
| // 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.
Is there AuthenticateAsync() that resolves the default from the service?
|
|
||
| // 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.
AutomaticScheme.
AuthenticateAsync()
| 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}" |
| }; | ||
| }); | ||
|
|
||
| services.AddAuthentication(sharedOptions => |
There was a problem hiding this comment.
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.
Its because Azure also has a log level so this is disambiguiating
| }); | ||
|
|
||
|
|
||
| services.AddAuthentication(sharedOptions => |
| } | ||
|
|
||
| // CookieAuthenticationOptions.AutomaticAuthenticate = true (default) causes User to be set | ||
| // DefaultAuthenticationScheme causes User to be set |
|
|
||
| // This is what [Authorize] calls | ||
| // var user = await context.Authentication.AuthenticateAsync(AuthenticationManager.AutomaticScheme); | ||
| // var user = await context.AuthenticateAsync(DefaultAuthenticateScheme); |
| 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>"); |
|
|
||
| services.AddAuthentication(sharedOptions => | ||
| { | ||
| sharedOptions.DefaultAuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme; |
| { | ||
| if (!ChallengeCalled && Options.AutomaticChallenge && Response.StatusCode == 401) | ||
| Events = Options.Events; | ||
| if (Options.EventsType != null) |
There was a problem hiding this comment.
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.
Yeah we have to do this because we always new up the events today.
There was a problem hiding this comment.
I just meant we should change it to if (Events == null && Options.EventsType != null)
There was a problem hiding this comment.
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.
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.
Fair enough, I'll nuke the review
| Response.OnStarting(OnStartingCallback, this); | ||
|
|
||
| if (ShouldHandleScheme(AuthenticationManager.AutomaticScheme, Options.AutomaticAuthenticate)) | ||
| Options = OptionsSnapshot.Get(Scheme.Name) ?? new TOptions(); |
There was a problem hiding this comment.
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.
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.
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.
Context is not used? It's not even null checked.
There was a problem hiding this comment.
Sweet, means we can nuke the type then
| } | ||
|
|
||
| _next = next; | ||
| _next = next ?? throw new ArgumentNullException(nameof(next)); |
| { | ||
| var handler = CreateHandler(); | ||
| await handler.InitializeAsync(Options, context, Logger, UrlEncoder); | ||
| var oldFeature = context.Features.Get<IAuthenticationFeature>(); |
There was a problem hiding this comment.
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.
"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.
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.
Move HandleRequestAsync processing above defaultAuthenticate. HandleRequestAsync may short circuit the request.
|
|
||
| await _next(context); | ||
| } | ||
| finally |
There was a problem hiding this comment.
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.
Still not a verb... IsFoo is used for bool properties. This might be good enough though...
There was a problem hiding this comment.
Yeah this is internal plumbing right? This typically isn't called by event handlers is it?
| { | ||
| /// <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. |
|
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
|
|
||
| public override async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) | ||
| { | ||
| await base.InitializeAsync(scheme, context); |
There was a problem hiding this comment.
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.
You mean if you override Initialize and don't call base?
There was a problem hiding this comment.
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.
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.
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.
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 :)
commented
Apr 18, 2017
|
Looks like there's still several smaller comments un-addressed, but otherwise you're ready to go. |
| public static IServiceCollection AddOpenIdConnectAuthentication(this IServiceCollection services) | ||
| => services.AddOpenIdConnectAuthentication(OpenIdConnectDefaults.AuthenticationScheme, _ => { }); | ||
| { | ||
| services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, OpenIdConnectConfigureOptions>(); |
There was a problem hiding this comment.
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.
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.
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.
But for auth we always have the scheme name, so we can always just use that.
There was a problem hiding this comment.
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.
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.
Okay. Anyway, this shouldn't be a problem, since the second call to AddOpenIdConnectAuthentication() should throw immediately.
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. |
commented
Apr 18, 2017
|
@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. |
commented
Apr 18, 2017
|
Will do, thanks as always for all the great feedback @PinpointTownes :) |
commented
Apr 20, 2017
|
@PinpointTownes 24524 should have these changes now. |
commented
Apr 20, 2017
|
@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