-
Notifications
You must be signed in to change notification settings - Fork 59
Auth 2.0 + IISListener #338
Conversation
@HaoK, |
I really wish we had a way of detecting at startup if/which WindowsAuth schemes were enabled in IIS. @shirhatti, @pan-wang? It's hard to implement a correct IIS auth handler without knowing, and we don't want people to have to configure it in two places. |
{ | ||
context.NotAuthenticated(); | ||
} | ||
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(user, Scheme.Name))); |
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.
Note we must not call Merge on this principal, we need to keep the original for IsInRole to work. Same for WebListener.
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.
Yuck, ok so that basically means you can't really combine the windows auth with the other auth types or the merging will break things
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, but those scenarios don't usually overlap. E.g. you don't do NTLM + Cookies at the same time. So long as we can make Merge smart enough not to merge when there's really just WindowsPrincipal and Anonymous it should be fine.
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 made sure this identity never gets merged 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 we'd have to add something to the security helper (or the authorize filter) to special case this somehow.
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 so by default we don't have to do anything, merge is only called if the effective policy ends up with multiple schemes requested, which requires a "config error" to have been introduced
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.
Main scenario = DefaultAuthenticate/ChallengeScheme = "Windows" 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.
You're registering both Negotiate and NTLM separately right now. The experience would be better if you combine them.
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.
Then you wouldn't have to specify a default
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.
Are we calling this Windows too like HttpSys?
return PriorHandler.SignInAsync(context); | ||
case ChallengeBehavior.Automatic: | ||
// If there is a principal already, invoke the forbidden code path | ||
if (GetUser(context.HttpContext) == 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.
GetUser needs to cache if this is going to work, it disposes the handle so it can't run more than once.
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.
Updated with GetUser caching
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.
It only caches on success though, not sure if we expect this to be called repeatedly on failures or if we care...
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 fine, the failure scenarios are primarily for a missing header which will be fast.
Updated with latests auth changes and packages, tests are all passing now |
var auth = authentication.FirstOrDefault(); | ||
if (auth == null) | ||
{ | ||
throw new InvalidOperationException("IServiceCollection.AddAuthentication() is required to use 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.
Remove this exception for now, ForwardWindowsAuthentication is enabled by default.
throw new InvalidOperationException("IServiceCollection.AddAuthentication() is required to use Authentication."); | ||
} | ||
|
||
auth.AddScheme(new AuthenticationScheme("NTLM", /*displayName*/ null,typeof(AuthenticationHandler))); |
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.
named params instead of inline comments?
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="$(AspNetCoreVersion)" /> | ||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="$(AspNetCoreVersion)" /> | ||
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="$(AspNetCoreVersion)" /> | ||
<PackageReference Include="Microsoft.AspNetCore.HttpOverrides" Version="$(AspNetCoreVersion)" /> | ||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(AspNetCoreVersion)" /> | ||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(AspNetCoreVersion)" /> |
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 duplicate
@@ -191,7 +201,7 @@ public void PathBaseHiddenFromServer() | |||
{ | |||
app.Run(context => | |||
{ | |||
var auth = context.Features.Get<IHttpAuthenticationFeature>(); | |||
var auth = context.RequestServices.GetService<IAuthenticationSchemeProvider>(); |
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 equivalent. You should still add the auth service in this case, it's testing the ForwardWindowsAuthentication flag. You may clone this test to make sure everything also works without the auth service.
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.
Updated test to verify handler only added when forward = true, also added a variant that ensures things don't blow up without auth services.
@@ -11,6 +11,8 @@ | |||
|
|||
<ItemGroup> | |||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Server.IISIntegration\Microsoft.AspNetCore.Server.IISIntegration.csproj" /> | |||
<ProjectReference Include="..\..\..\Security\src\Microsoft.AspNetCore.Authentication\Microsoft.AspNetCore.Authentication.csproj" /> |
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 :-)
using Microsoft.Extensions.Logging; | ||
|
||
namespace TestSites | ||
{ | ||
public class StartupHttpsHelloWorld | ||
{ | ||
public void ConfigureServices(IServiceCollection services) | ||
{ | ||
services.AddAuthenticationCore(); |
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.
Auth shouldn't be needed in this scenario
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.
It is because of the functional tests calling Challenge
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 yes only needed in the Ntlm one.
test/TestSites/StartupHelloWorld.cs
Outdated
using Microsoft.Extensions.Logging; | ||
|
||
namespace TestSites | ||
{ | ||
public class StartupHelloWorld | ||
{ | ||
public void ConfigureServices(IServiceCollection services) | ||
{ | ||
services.AddAuthenticationCore(); |
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.
Auth shouldn't be needed in this scenario
var negotiate = await auth.GetSchemeAsync("Negotiate"); | ||
Assert.NotNull(negotiate); | ||
Assert.Equal("Microsoft.AspNetCore.Server.IISIntegration.AuthenticationHandler", negotiate.HandlerType.FullName); | ||
var windows = await auth.GetSchemeAsync("Windows"); |
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.
File a followup issue to figure out how we can optionally handle granular schemes. This will be more relevant for HttpSys, but the solution will likely apply to both.
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.
@@ -24,6 +25,7 @@ public class IISMiddlewareTests | |||
.UseSetting("PORT", "12345") | |||
.UseSetting("APPL_PATH", "/") | |||
.UseIISIntegration() | |||
.ConfigureServices(services => services.AddAuthentication()) |
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.
Only add this to tests that need it?
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.
Yup, done
Assert.NotNull(auth); | ||
Assert.Equal("Microsoft.AspNetCore.Server.IISIntegration.AuthenticationHandler", auth.Handler.GetType().FullName); | ||
var windowsAuth = await auth.GetSchemeAsync("Windows"); | ||
if (forward) |
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.
else assert null? or would GetSchemeAsync throw?
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.
Yup I forgot to add the else check null, thanks :)
@@ -150,15 +156,62 @@ public void PathBaseHiddenFromServer() | |||
.UseSetting("PORT", "12345") | |||
.UseSetting("APPL_PATH", "/") | |||
.UseIISIntegration() | |||
.ConfigureServices(services => services.AddAuthentication()) |
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.
make a copy of this test without AddAuthentication and test auth.GetSchemeAsync("Windows"); returns 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.
Well we can't even call GetScheme without AddAuthentication as the SchemeProvider will be null, we already have tests that verify that the server can start without AddAuthentication.
} | ||
|
||
if (context.Request.Path.Equals("/AutoForbid")) | ||
{ | ||
return context.Authentication.ChallengeAsync(); | ||
return context.ChallengeAsync(); | ||
} | ||
|
||
if (context.Request.Path.Equals("/RestrictedNegotiate")) |
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 can remove RestrictedNegotiate, it's not used in the tests.
IISIntegration/test/IISIntegration.FunctionalTests/NtlmAuthentationTest.cs
Lines 97 to 102 in e2537e1
response = await httpClient.GetAsync("/RestrictedNTLM"); | |
responseText = await response.Content.ReadAsStringAsync(); | |
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); | |
Assert.Contains("NTLM", response.Headers.WwwAuthenticate.ToString()); | |
// Note we can't restrict a challenge to a specific auth type, the native auth modules always add themselves. | |
Assert.Contains("Negotiate", response.Headers.WwwAuthenticate.ToString()); |
samples/IISSample/Startup.cs
Outdated
@@ -12,6 +13,7 @@ public class Startup | |||
{ | |||
public void ConfigureServices(IServiceCollection services) | |||
{ | |||
services.AddAuthentication(); |
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 also need to add the auth middleware if you want HttpContext.User to get set without calling Authenticate, right? E.g. this sample doesn't work right now does it? Oh, the auth middelware is in the security repo, you can't reference it here can you?
HttpSysServer doesn't have this problem because it sets User before the app is invoked. What if the IISMiddleware always set the User? That code in GetUser should be run on every request regardless to clean up handles. The IISMiddleware could resolve the auth service, resolve the handler, and invoke it directly.
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.
Didn't try running the samples, perhaps mistakenly I assumed functional tests were a superset, urgh yeah forgot we can't depend on security in this repo :/
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.
Windows auth doesn't really need the AuthN middleware, it could do it itself / resolve and invoke the service directly.
Functional tests are failing, but other tests are passing
Updated as we discussed, if ForwardWindowsAuth, add Windows scheme, and always set it to httpContext.User if found. |
await _next(httpContext); | ||
} | ||
finally | ||
var result = await httpContext.AuthenticateAsync("Windows"); |
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.
Constant for Windows. Public static readonly?
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.
IISMiddleware.AuthenticationScheme
@@ -76,6 +76,7 @@ public Task NtlmAuthentication(RuntimeArchitecture architecture, ApplicationType | |||
Assert.Equal(HttpStatusCode.OK, response.StatusCode); | |||
Assert.Equal("Anonymous?True", responseText); | |||
|
|||
/* Disabled for now |
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 the skip url here
samples/IISSample/Startup.cs
Outdated
@@ -12,6 +13,7 @@ public class Startup | |||
{ | |||
public void ConfigureServices(IServiceCollection services) | |||
{ | |||
services.AddAuthenticationCore(); |
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.
This can be removed from samples and tests now that it's added by the main component.
test/TestSites/TestSites.csproj
Outdated
@@ -12,6 +12,7 @@ | |||
|
|||
<ItemGroup> | |||
<PackageReference Include="Microsoft.AspNetCore.AspNetCoreModule" Version="$(AspNetCoreModuleVersion)" /> | |||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Core" Version="$(AspNetCoreVersion)" /> |
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.
extra?
samples/IISSample/IISSample.csproj
Outdated
@@ -11,6 +11,7 @@ | |||
</ItemGroup> | |||
|
|||
<ItemGroup> | |||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Core" Version="$(AspNetCoreVersion)" /> |
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.
extra?
Updated |
@@ -18,6 +18,8 @@ namespace Microsoft.AspNetCore.Server.IISIntegration | |||
{ | |||
public class IISMiddleware | |||
{ | |||
public static readonly string AuthenticationScheme = "Windows"; |
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.
Do you want to follow the pattern from Security? E.g. IISDefaults.AuthenticationScheme?
https://github.com/aspnet/Security/blob/dev/src/Microsoft.AspNetCore.Authentication.Google/GoogleDefaults.cs#L11
Note those are public const string so they can be used in attributes.
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.
Sure IISDefaults.AuthenticationScheme, and HttpSysDefaults.AuthenticationScheme for the other repo?
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.
Sure.
{ | ||
Assert.NotNull(windowsAuth); | ||
Assert.Null(windowsAuth.DisplayName); | ||
Assert.Equal("AuthenticationHandler", windowsAuth.HandlerType.Name); |
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.
Vague?
[Theory] | ||
[InlineData(true)] | ||
[InlineData(false)] | ||
public async Task DoesNotBlowUpWithoutAuth(bool forward) |
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 relevant anymore? Auth is always added.
You'll need to update ServerTests too. |
@Tratcher
Given that ForwardWindows is on by default, not sure making auth optional is going to fly. All of the tests blew up with Auth missing until I added them all...