Repository navigation
Conversation
|
Hi @HaoK, I'm your friendly neighborhood .NET Foundation Pull Request Bot (You can call me DNFBOT). Thanks for your contribution! The agreement was validated by .NET Foundation and real humans are currently evaluating your PR. TTYL, DNFBOT; |
|
Rather than nuking the existing auth package first, requiring updating everything at once, implementation of the new stack will be in a Authentication.Impl package until everything is done..
|
|
@leastprivilege and I have a TODO item to look over and give feedback. We were hoping to have done this last week, but it didn't happen. Perhaps this weekend or next week. Thanks. |
|
Sure, you guys might want to look at the old PR first for better context/info, and it has more or less the same shape, it will take a bit for me to stitch everything back together again in this PR. |
|
Seems too confusing to have any dependency on the old HttpAuthentication feature, going to copy ChallengeBehavior and AuthenticationProperties instead, since we aren't supporting old and new together. |
|
|
||
| [Fact] | ||
| public async Task MapWillNotAffectChallenge() | ||
| public async Task MapWillAffectChallenge() |
There was a problem hiding this comment.
Note: this is a behavior change from old stack to new stack (Map does affect redirect url for challenge now that its not tied to any middleware)
There was a problem hiding this comment.
Be careful, it's most likely not a wanted change: if /page is configured as the callback path and the user is actually redirected to /login/page, all he'll get is a 404 response, as no login page will handle the request.
I assume it similarly impacts all the RAT-based middleware that use callback paths and have built-in endpoints? (OIDC, OAuth2, Twitter...)
|
Cookies and tests have been updated to use the new stack, and pull system clock from DI |
| } | ||
|
|
||
| [Fact] | ||
| [ConditionalFact(Skip = "Revisit, exception no longer thrown")] |
There was a problem hiding this comment.
Hum, why? The InvalidOperationException is there to inform the dev' that something is wrong with his setup. I don't think it's preferable to silently ignore the operation.
There was a problem hiding this comment.
I think the old test threw because it was doing an AutomaticChallenge followed by an second challenge, which no longer happens in the new stack, I'll have to debug the old test, to see what was triggering it
| [Theory] | ||
| [InlineData(true)] | ||
| [InlineData(false)] | ||
| public async Task MapWillAffectChallengeOnlyWithUseAuth(bool useAuth) |
There was a problem hiding this comment.
FYI @PinpointTownes fixed the map + resolve url behavior to work similar to before (using the path of the single middleware), via a new IAuthenticationFeature like we discussed @Tratcher
| "windowsAuthentication": false, | ||
| "anonymousAuthentication": true, | ||
| "iisExpress": { | ||
| "applicationUrl": "http://localhost:42023", |
There was a problem hiding this comment.
revert, these samples need to stay on a specific port.
| { | ||
| public AuthenticationScheme(string name, Type handlerType, Dictionary<string, object> settings) | ||
| { | ||
| Name = name ?? throw new ArgumentNullException(nameof(name)); |
| { | ||
| public class AuthenticationScheme | ||
| { | ||
| public AuthenticationScheme(string name, Type handlerType, Dictionary<string, object> settings) |
There was a problem hiding this comment.
Why not just take an IReadOnlyDictionary?
|
|
||
| namespace Microsoft.AspNetCore.Authentication | ||
| { | ||
| public enum ChallengeBehavior |
There was a problem hiding this comment.
Looking to eliminate this. Let Challenge and Forbidden be separate code paths, and Automatic would be resolved at a higher level.
There was a problem hiding this comment.
Yeah but we likely can't do this until there's a bunch of corresponding AuthZ changes, we should tackle that in a follow on PR specifically for AuthZ (see #901)
| </PropertyGroup> | ||
| <ItemGroup> | ||
| <PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="1.2.0-*" /> | ||
| <PackageReference Include="Microsoft.AspNetCore.Http" Version="1.2.0-*" /> |
There was a problem hiding this comment.
What did you need from .HTTP? It's unusual for an Abstractions package to have this many concrete dependencies.
There was a problem hiding this comment.
Good catch, these were just carried over from eixsting Authentication.csproj, I switched them to just the Http.Abstractions package and things were fine.
| using Microsoft.Extensions.Options; | ||
|
|
||
| namespace Microsoft.AspNetCore.Builder | ||
| namespace Microsoft.AspNetCore.Authentication.Cookies |
There was a problem hiding this comment.
We'd used this namespace to improve intellisence in Startup, it's the same namespace the extensions were in. Should these move to the DI namespace?
There was a problem hiding this comment.
I don't think that's necessary with lambdas, as intellisense knows the type from the signature, the namespace hack was only needed for new
|
|
||
| public CookieAuthenticationOptions Options { get; } | ||
|
|
||
| public AuthenticationScheme Scheme { get; } |
There was a problem hiding this comment.
Would Scheme go on BaseAuthenticationContext?
There was a problem hiding this comment.
I tried to bridge the context's between the service and the handlers. The service doesn't care about the scheme object, only the string, but maybe we should just have a BaseAuthenticationHandlerContext that does have the scheme. I have a todo in the top comment to make a pass through all of our events/context objects since they have always been a bit of a hodgepodge mess
| throw new ArgumentNullException(nameof(options)); | ||
| } | ||
|
|
||
| if (string.IsNullOrEmpty(Options.AppId)) |
There was a problem hiding this comment.
Grrr yup, I'll add a test for these as well
There was a problem hiding this comment.
Moved the checks to Validate in the options which at least causes an exception, but the current behavior isn't perfect, in that it blows up the first time the scheme is used in the auth service.
There was a problem hiding this comment.
Weren't we going to call Validate from the UseFooAuth extension too?
There was a problem hiding this comment.
Sorry I mispoke, it blows up when the Auth service is constructed as the schemes are built when SchemeProvider/HandlerResolvers are built. So maybe that's not that bad if the auth service itself cannot be resolved due to a configuration error.
There was a problem hiding this comment.
Can you share the callstack/exception?
There was a problem hiding this comment.
typeof(System.ArgumentException): The 'ClientId' option must be provided.
Parameter name: ClientId
Stack Trace:
at Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions.Validate() in C:\GitHub\Security\src\Microsoft.AspNetCore.Authentication.OAuth\OAuthOptions.cs:line 33
at Microsoft.Extensions.DependencyInjection.AuthenticationServiceCollectionExtensions.<>c__DisplayClass2_0`2.<AddSchemeHandler>b__1(AuthenticationSchemeBuilder b) in C:\GitHub\Security\src\Microsoft.AspNetCore.Authentication.Impl\AuthenticationServiceCollectionExtensions.cs:line 66
at Microsoft.AspNetCore.Authentication.AuthenticationOptions.AddScheme(String name, Action`1 configureBuilder) in C:\GitHub\Security\src\Microsoft.AspNetCore.Authentication.Abstractions\AuthenticationOptions.cs:line 42
at Microsoft.Extensions.DependencyInjection.AuthenticationServiceCollectionExtensions.<>c__DisplayClass2_0`2.<AddSchemeHandler>b__0(AuthenticationOptions o) in C:\GitHub\Security\src\Microsoft.AspNetCore.Authentication.Impl\AuthenticationServiceCollectionExtensions.cs:line 56
at Microsoft.Extensions.Options.OptionsCache`1.CreateOptions()
at System.Threading.LazyInitializer.EnsureInitializedCore[T](T& target, Boolean& initialized, Object& syncLock, Func`1 valueFactory)
at Microsoft.Extensions.Options.OptionsCache`1.get_Value()
at Microsoft.AspNetCore.Authentication.DefaultAuthenticationSchemeProvider..ctor(IOptions`1 options) in C:\GitHub\Security\src\Microsoft.AspNetCore.Authentication.Impl\DefaultAuthenticationSchemeProvider.cs:line 18
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, ServiceProvider provider)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProvider provider)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, ServiceProvider provider)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProvider provider)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, ServiceProvider provider)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProvider provider)
at Microsoft.Extensions.DependencyInjection.ServiceProvider.<>c__DisplayClass17_0.<RealizeService>b__0(ServiceProvider provider)
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
at Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.ChallengeAsync(HttpContext context, String scheme, AuthenticationProperties properties, ChallengeBehavior behavior) in C:\GitHub\Security\src\Microsoft.AspNetCore.Authentication.Abstractions\AuthenticationHttpContextExtensions.cs:line 23
at Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.ChallengeAsync(HttpContext context, String scheme, AuthenticationProperties properties) in C:\GitHub\Security\src\Microsoft.AspNetCore.Authentication.Abstractions\AuthenticationHttpContextExtensions.cs:line 20
at Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.ChallengeAsync(HttpContext context, String scheme) in C:\GitHub\Security\src\Microsoft.AspNetCore.Authentication.Abstractions\AuthenticationHttpContextExtensions.cs:line 17
at Microsoft.AspNetCore.Authentication.OAuth.OAuthTests.<>c__DisplayClass0_0.<ThrowsIfClientIdMissing>b__4() in C:\GitHub\Security\test\Microsoft.AspNetCore.Authentication.Test\OAuthTests.cs:line 33
There was a problem hiding this comment.
That only says OAuth, it doesn't tell you Facebook. And Facebook calls it AppId. This is going to cause confusion.
There was a problem hiding this comment.
I copied a different exception failure for the above example from the oauth tests, the facebook one says app id and has a facebookoptions.validate at the top.
| { | ||
| // Only renew if requested, and neither sign in or sign out was called | ||
| if (!_shouldRefresh || SignInAccepted || SignOutAccepted) | ||
| if (!_shouldRefresh || SignInCalled || SignOutCalled) |
There was a problem hiding this comment.
Test hole here, need to add a test that verifies cookies aren't renewed if SignIn or SignOut is called. (this was broken before this change)
|
We also had lost the OauthOptions validations too which had no test coverage :/ I added some basic validation tests for those, also compact'ed the test structure, I nuked all the folders that had a single file, so basically only OpenIdConnect has a subfolder in Auth.Test now. Also started dropping middleware from the file names/comments where I see it. |
|
Hi all -- again, no time yet to have a look, but I wanted to let you know how we're integrating with the current security middleware in IdentityServer. In short, we're registering our own authentication middleware to be able to do a decorator pattern like feature to applications hosting us. We'd like to know if we can do something similar in this v2 work. Here's the main handler we have now: |
|
@brockallen the "authentication middleware" chain is no longer a thing in this prototype, so you won't be able to port your handler as-is. That said, I guess you could achieve something similar by replacing |
Right, but then we'd probably need to decorate whatever implementation is provided by default. Too bad the core DI system is so lacking in so many useful features. |
There a few workarounds to make the default DI stack (kinda) work with the decorator pattern, but it's indeed not ideal... I guess that's the price to pay for having full DI here
|
|
Well, the original chained handler design in katana was essentially an attempt at designing a poor man's decorator pattern in a system that had no DI. Moving to a service oriented approach in a DI system with missing features is just changing the surrounding code. I think I agree that it's not addressing the fundamental requirement. I've yet to look into the other changes to see if they've done anything to make the overall design better. |
|
@brockallen it looks like you could just replace the DefaultAuthenticationService with your own derived one, something like this: public class AuthenticationService : DefaultAuthenticationService, IAuthenticationService
{
private readonly IdentityServerOptions _options;
private readonly IHttpContextAccessor _context;
private readonly ISessionIdService _sessionId;
private readonly ILogger<AuthenticationHandler> _logger;
private readonly IEventService _events;
public AuthenticationService(
IAuthenticationHandlerResolver cache,
IClaimsTransformation transform,
IHttpContextAccessor context,
IdentityServerOptions options,
ISessionIdService sessionId,
IEventService events,
ILogger<AuthenticationService> logger) : base(cache, transform)
{
_context = context;
_options = options;
_sessionId = sessionId;
_events = events;
_logger = logger;
}
public override async Task SignInAsync(SignInContext context)
{
if (context.AuthenticationScheme == _options.Authentication.EffectiveAuthenticationScheme)
{
AugmentContext(context);
await RaiseSignInEventAsync(context.Principal);
}
await base.SignInAsync(context);
}
private void AugmentContext(SignInContext context)
{
_logger.LogDebug("Augmenting SignInContext");
context.Principal.AssertRequiredClaims();
context.Principal.AugmentMissingClaims();
_sessionId.CreateSessionId(context);
}
public override async Task SignOutAsync(SignOutContext context)
{
if (context.AuthenticationScheme == _options.Authentication.EffectiveAuthenticationScheme)
{
_sessionId.RemoveCookie();
await RaiseSignOutEventAsync();
}
await base.SignOutAsync(context);
}
// Would need to move to your own middleware most likely
internal async Task InitAsync()
{
var auth = GetAuthentication();
_handler = auth.Handler;
auth.Handler = this;
await _sessionId.EnsureSessionCookieAsync();
}
internal void Cleanup()
{
var auth = GetAuthentication();
auth.Handler = _handler;
}
// No longer needed, DI registration
IHttpAuthenticationFeature GetAuthentication()
{
var auth = _context.HttpContext.Features.Get<IHttpAuthenticationFeature>();
if (auth == null)
{
auth = new HttpAuthenticationFeature();
_context.HttpContext.Features.Set(auth);
}
return auth;
}
private async Task RaiseSignInEventAsync(ClaimsPrincipal principal)
{
await _events.RaiseLoginEventAsync(principal);
}
private async Task RaiseSignOutEventAsync()
{
var principal = await _context.HttpContext.GetIdentityServerUserAsync();
if (principal.IsAuthenticated())
{
await _events.RaiseLogoutEventAsync(principal);
}
}
}
} |
|
Updated: schemes must now explicitly register callback paths in their scheme data. Schemes ProcessRequest will only be called if Request.Path matches a callback path they specify. Will probably remove HandleRemoteAuthenticate since its no longer needed. Also will change the base ProcessRequest to throw now that it shouldn't be invoked anymore |
|
How terrible would it be to pull in actual routing? |
|
Well we could add something like "routes.MapAuthenticationRoutes()". We'd still have to have some internal plumbing in the schemes to store the routes for this method to consume. So the required startup pattern would then become: Doesn't look too unreasonable, but is it reasonable for us to require routing for auth to work? |
|
Not bad. How well would that work with dynamically added/removed auth schemes? |
|
I'm not sure, depends on how well routing supports dynamically adding routes :) Let me see |
|
Forking my comment as it was hidden by GitHub: @HaoK point taken, tho' I'm not sure why you'd want to support scenarios with "hundreds/thousands of schemes" if there's no real support in the options stack (I'm not sure having hundreds of schemes + hundreds of handlers + hundreds of options instances is really scalable). Not being able to invoke my handler for every path is really annoying for me. As a compromise, would you consider bringing back your per-scheme "promiscuous mode"? (aka |
|
I'm not an routing expert, but it looks if we forced a pattern on the callback paths to fit into a route constraint, say |
|
@PinpointTownes its going to end up being a question of how easy we want to make it to have a global handler like that. We certainly could have a handleAllRequests flag on AuthenticationScheme that defaults to false. |
|
Started cleaning up some of the legacy handler processing.
I'm considering introducing a new ProcessRequestResult which would let us elimate the funky request processing that shows up in AuthenticateResult today since that's the main return type for Auth as a whole. |
That would be much appreciated 😅
Hum, but then, what's the difference between |
|
Difference between HandledResponse and StopProcessing is whether ProcessRequest returns true/false, same as today's handledResponse/Skip. Controls whether the middleware continues to invoke other handlers or not via the ghetto routing :) |
|
I'm confused now. When Yet, returning So AFAICT, your renamed |
|
Fair enough, it was mostly the SkipToNextMiddleware that I was renaming, since the emphasis should just be that its skipping any further processing in this handler. What happens after that is the normal execution. We can keep it as Continue/Handled/Skip as the three states which correspond to (false, true, false for the AuthMiddleware invoking the rest of the pipeline) |
|
The confusing part is that StopProcessing is the reasonable name from the perspective of the individual handler's usage of this state, since it lives in the BaseControlContext. I'll leave that method name still as ProcessingCompleted since for both HandledResponse + Skipped, that means true for the purposes of the handler who is using the context. All of this smells a bit to me which is why I'm playing around with this area right now... |
|
Ok I revereted the StopProcessing state back to Skip. So the net result of these changes are: AuthenticateResult.Skip => AuthenticateResult.None to signify no failure, no ticket, nothing |
|
Some implications if we try to use the routing middleware instead of our own ghetto routing:
Or Without MVC, this overload could add routing and configure it?
|
|
See #1144 for an iteration using IOptionsFactory which provides named options (and potentially validation). It also split the ProcessRequest into a different interface IAuthenticationRequestHandler which is used instead of CallbackPaths to determine which schemes get called by the authentication middleware |
Tratcher
left a comment
There was a problem hiding this comment.
Some feedback. Still reviewing.
| { | ||
| // 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.
No more generic Challenge? It should work if there's only one or its the default.
There was a problem hiding this comment.
We can preserve it, it would line up with naked Authorize with no DefaultPolicy then.
There was a problem hiding this comment.
Well if we add an overload for Challenge that omits scheme, we should probably do the same for authenticate and SignIn, basically null scheme will mean use default (since the overloads are implemented as extension methods)
| } | ||
|
|
||
| await context.Authentication.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, | ||
| await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, |
There was a problem hiding this comment.
So AuthenticationManager is completely obsolete and you're rooting all of the extensions on HttpContext?
There was a problem hiding this comment.
Yeah we should deprecate the Authentication versions as part of this
|
|
||
| services.AddAuthentication(sharedOptions => | ||
| sharedOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme); | ||
| sharedOptions.DefaultAuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme); |
There was a problem hiding this comment.
DefaultSignInScheme. You also need to set DefaultAuthenticationScheme and DefaultChallengeScheme right?
Same for the AzureAd sample.
There was a problem hiding this comment.
Yup, updating these in the other PR
| SaveTokens = true, | ||
| o.AppId = Configuration["facebook:appid"]; | ||
| o.AppSecret = Configuration["facebook:appsecret"]; | ||
| o.Scope.Clear(); |
There was a problem hiding this comment.
hmm, syntax usage regression.
I don't think the old synatax was Clearing, just Adding.
| ClientId = Configuration["microsoftaccount:clientid"], | ||
| ClientSecret = Configuration["microsoftaccount:clientsecret"], | ||
| SaveTokens = true | ||
| o.DisplayName = "MicrosoftAccount"; |
There was a problem hiding this comment.
Why is the display name set for this one? Isn't this the default value.
| public Type HandlerType { get; } | ||
|
|
||
| /// <summary> | ||
| /// If true, the AuthenticationMiddleware will call the handler's HandleRequestAsync method. |
There was a problem hiding this comment.
Many of the comments were already obsolete, we'll need to do a full scrub as part of this
|
|
||
| // Holds things like the configured options instances for the handler | ||
| // Also replacement for AuthenticationDescription | ||
| public IReadOnlyDictionary<string, object> Settings { get; } |
There was a problem hiding this comment.
This is really weird. Are we using it for anything besides Options and Description? Why not just expose those as first class properties?
There was a problem hiding this comment.
No, but I nuked AuthenticationDescription which was similar and unused mostly. We can put the DisplayName as a property on the scheme itself or inside of here like Description used to...
| /// <summary> | ||
| /// Gets the claims-principal with authenticated user identities. | ||
| /// </summary> | ||
| public ClaimsPrincipal Principal{ get; private set; } |
| public AuthenticationTicket(ClaimsPrincipal principal, AuthenticationProperties properties, string authenticationScheme) | ||
| { | ||
| AuthenticationScheme = authenticationScheme; | ||
| Principal = principal ?? throw new ArgumentNullException(nameof(principal)); |
| public class AuthenticationToken | ||
| { | ||
| public string Name { get; set; } | ||
| public string Value { get; set; } |
There was a problem hiding this comment.
Make immutable and set via constructor?
| private readonly AuthenticationOptions _options; | ||
| private readonly object _lock = new object(); | ||
|
|
||
| private IDictionary<string, AuthenticationScheme> _map = new Dictionary<string, AuthenticationScheme>(); // case sensitive? |
| { | ||
| if (!_handlerMap.ContainsKey(path)) | ||
| { | ||
| _handlerMap[path] = new List<AuthenticationScheme>(); |
There was a problem hiding this comment.
List? I thought the point was that it would be a unique mapping. Urls usually map to a unique resource.
| /// <summary> | ||
| /// Builds the actual AuthenticationScheme instances from the AuthenticationOptions. | ||
| /// </summary> | ||
| public class DefaultAuthenticationSchemeProvider : IAuthenticationSchemeProvider |
There was a problem hiding this comment.
AuthenticationSchemeProvider, remove Default
There was a problem hiding this comment.
Good call, dropped Default prefix across the board
| { | ||
| throw new InvalidOperationException("Scheme already exists: " + scheme.Name); | ||
| } | ||
| lock (_lock) |
There was a problem hiding this comment.
Locking on modifications is insufficient, you also have to lock on lookups.
| await handler.SignOutAsync(signOutContext); | ||
| } | ||
|
|
||
| public virtual Task SignInAsync(HttpContext httpContext, string authenticationScheme, ClaimsPrincipal principal) |
There was a problem hiding this comment.
Yeah unneeded these already exists as extensions on httpContext, I'll remove
| return SignInAsync(httpContext, authenticationScheme, principal, properties: null); | ||
| } | ||
|
|
||
| public virtual Task ForbidAsync(HttpContext httpContext, string authenticationScheme) |
| /// <summary> | ||
| /// Default claims transformation is a no-op. | ||
| /// </summary> | ||
| public class DefaultClaimsTransformation : IClaimsTransformation |
|
|
||
| namespace Microsoft.AspNetCore.Authentication | ||
| { | ||
| public class DefaultAuthenticationService : IAuthenticationService |
|
Replaced with #1151 |
Test showing dynamic add/remove of schemes: d92d923#diff-220317c67a0ea163cf751157519c7f66R29
cc @Eilon @Tratcher @davidfowl
Follow up items:
Test holes: