Reputation: 1376
I am implementing OpenID Connect authentication for my ASP.Net Core 2.0 web app. The app runs inside an IIS site with Windows Authentication enabled. In the cases when the user accesses the app while being logged in to the corporate network, I would like to spare them from having to enter their username into the Azure AD dialog, but rather have the app derive it from the username stored in HttpContext and pass it to the AD sign in link as a login_hint
. Below is the code with which I am trying to achieve the desired behavior:
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddOpenIdConnect(options =>
{
options.Authority = "https://login.microsoftonline.com/<removed>";
options.ClientId = "<removed>";
options.ResponseType = OpenIdConnectResponseType.IdToken;
options.CallbackPath = "/auth/signin-callback";
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = context =>
{
if(context?.HttpContext?.User?.Identity?.Name != null)
context.ProtocolMessage.SetParameter("login_hint", context.HttpContext.User.Identity.Name.Replace("domain\\", "") + "@domain.com");
return Task.FromResult(0);
}
};
}
).AddCookie();
However, this doesn't work. As soon as the user hits a controller protected with the [Authorize]
attribute, the framework sees that the user is already authenticated by IIS and does not redirect to Azure AD sign-in.
If I specify the authentication scheme in the attribute
[Authorize(AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme)]
the app redirects to AD sign in, but context.HttpContext.User.Identity.Name
is null
, which indicates that IIS authentication did not happen.
I also tried disabling automatic authentication in the ConfigureServices
method of Startup.cs
services.Configure<IISOptions>(options => {options.AutomaticAuthentication = false;});
to the same effect -- OpenID Connect kicks in, but context.HttpContext.User.Identity.Name
is null
. Same happens if I run the app on Kestrel or turn off Windows Auth on the IIS site.
Is there a way to have it both ways -- have IIS Authentication happen when it is available, but still proceed with OpenId Connect while using the identity established by IIS for the login_hint parameter?
Thanks in advance!
Upvotes: 3
Views: 6122
Reputation: 1376
Thanks to @mode777 I have found a solution -- I added this code to my Home controller:
var oidcAuthResult = HttpContext.AuthenticateAsync(OpenIdConnectDefaults.AuthenticationScheme).Result;
if (oidcAuthResult.Principal == null)
return Challenge(OpenIdConnectDefaults.AuthenticationScheme);
So, the full code for the Index action of my Home controller (the default route for the app) is simply:
[Authorize]
public IActionResult Index()
{
var oidcAuthResult = HttpContext.AuthenticateAsync(OpenIdConnectDefaults.AuthenticationScheme).Result;
if (oidcAuthResult.Principal == null)
return Challenge(OpenIdConnectDefaults.AuthenticationScheme);
return View();
}
The ConfigureServices method stays unchanged, like specified in the question:
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddOpenIdConnect(options =>
{
options.Authority = "https://login.microsoftonline.com/<removed>";
options.ClientId = "<removed>";
options.ResponseType = OpenIdConnectResponseType.IdToken;
options.CallbackPath = "/auth/signin-callback";
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = context =>
{
if(context?.HttpContext?.User?.Identity?.Name != null)
context.ProtocolMessage.SetParameter("login_hint", context.HttpContext.User.Identity.Name.Replace("domain\\", "") + "@domain.com");
return Task.FromResult(0);
}
};
}
).AddCookie();
So now the authentication flow works like this:
If the app is running under IIS with Windows Auth enabled, [Authorize] attribute will be satisfied, so the execution will enter the body of the Index action at which point it will be forced to authenticate via OIDC and the code in ConfigureServices will use the identity provided by Windows Auth to set a login_hint. If the app runs on a server that does not support Windows Auth (or has it disabled), the [Authorize] attribute will engage OIDC right away and the if
statement in the controller will prevent from redirecting to AD twice.
Perfect!
Thanks again @mode777.
Upvotes: 1
Reputation: 3197
If I understand correctly, you are trying to do Windows authentication first, read the username and pass it on to the Azure AD.
My gueass is, that if you force the OIDC scheme with [Authorize(AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme)]
, IIS will no longer perform IWA (Integrated Windows Authentication) for you.
Here are some things you could try:
At least for classic ASP.NET apps you could go to the IIS site dashboard and under Authentication
could disable "Anonymous Authentication", while enabling "Windows Authentication". This would force the user to always authenticate with Windows and might not be what you are looking for.
You can manually challenge Windows Authentication in a controller action before the redirect. Here is how I would do it (Core 1.1 might be slightly different now).
// Clear the existing external cookie to ensure a clean login process
await HttpContext.Authentication.SignOutAsync(_externalCookieScheme);
WindowsIdentity windowsIdentity = null;
if (_tef4AuthenticationOptions.EnableWindowsAuthentication)
{
// see if windows auth has already been requested and succeeded
var result = await HttpContext.Authentication.AuthenticateAsync(ServerConstants.WindowsScheme);
if (result is WindowsPrincipal)
windowsIdentity = result.Identity as WindowsIdentity;
else
return Challenge(ServerConstants.WindowsScheme);
}
Upvotes: 2