I have a MVC client accessing a Web API protected by IDS4. They all run on my local machine and hosted by IIS. The app works fine when using local identity for authentication. But when I try to use Windows authentication, I keep getting "401 Unauthorized" error from the dev tool and the login box keeps coming back to the browser.
Here is the Windows Authentication IIS setting
and enabled providers
It's almost like that the user ID or password was wrong, but that's nearly impossible because that's the domain user ID and password I use for logging into the system all the time. Besides, according to my reading, Windows Authentication is supposed to be "automatic", which means I will be authenticated silently without a login box in the first place.
Update
I enabled the IIS request tracing and here is the result from the log:
As you can see from the trace log item #29, the authentication (with the user ID I typed in, "DOM\Jack.Backer") was successful. However, some authorization item (#48) failed after that. And here is the detail of the failed item:
What's interesting is that the ErrorCode says that the operation (whatever it is) completed successfully, but still I received a warning with a HttpStatus=401 and a HttpReason=Unauthorized. Apparently, this is what failed my Windows Authentication. But what is this authorization about and how do I fix it?
In case anyone interested - I finally figured this one out. It is because the code that I downloaded from IndentityServer4's quickstart site in late 2020 doesn't have some of the important pieces needed for Windows authentication. Here is what I had to add to the Challenge function of the ExternalController class
and here is the ProcessWindowsLoginAsync function
private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl)
{
var result = await HttpContext.AuthenticateAsync(AccountOptions.WindowsAuthenticationSchemeName);
if (result?.Principal is WindowsPrincipal wp)
{
var props = new AuthenticationProperties()
{
RedirectUri = Url.Action(nameof(Callback)),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", AccountOptions.WindowsAuthenticationSchemeName },
}
};
var id = new ClaimsIdentity(AccountOptions.WindowsAuthenticationSchemeName);
id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name));
id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));
if (AccountOptions.IncludeWindowsGroups)
{
var wi = wp.Identity as WindowsIdentity;
var groups = wi.Groups.Translate(typeof(NTAccount));
var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
id.AddClaims(roles);
}
await HttpContext.SignInAsync(IdentityConstants.ExternalScheme, new ClaimsPrincipal(id), props);
return Redirect(props.RedirectUri);
}
else
{
return Challenge(AccountOptions.WindowsAuthenticationSchemeName);
}
}
Now my windows authentication works with no issues.
Related
Currently i am working on one POC with Identity server4 where i have to show my own login page if windows authentication get failed(in this case i just want to show my own login page and avoid browser login popup .
My question is where to inject my own login page in code? and how application will know windows authentication get failed?If you check below code, first request to AuthenticateAsync always return null and then it call Challenge from else block which ask browser to send Kerberos token
and we achieve SSO but now i want to show my own login page if SSO fail.
My scenario is exactly similar like this
Anyone know how to achieve this?
private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl)
{
// see if windows auth has already been requested and succeeded.
var result = await HttpContext.AuthenticateAsync(_windowsAuthConfig.WindowsAuthenticationProviderName);
if (result?.Principal is WindowsPrincipal wp)
{
var props = new AuthenticationProperties
{
RedirectUri = Url.Action("Callback"),
Items =
{
{ "returnUrl", returnUrl},
{ "scheme", _windowsAuthConfig.WindowsAuthenticationProviderName}
}
};
var id = new ClaimsIdentity(_windowsAuthConfig.WindowsAuthenticationProviderName);
var claims = await _userStore.GetClaimsForWindowsLoginAsync(wp);
id.AddClaims(claims);
_logger.LogDebug("Signing in user with windows authentication.");
await HttpContext.SignInAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme,new ClaimsPrincipal(id),props);
return Redirect(props.RedirectUri);
}
else
{
_logger.LogDebug("Re-triggered windows authentication using ChallengeResult.");
// Trigger windows auth
// since windows auth don't support the redirect uri,
// this URL is re-triggered when we call challenge
return Challenge(_windowsAuthConfig.WindowsAuthenticationSchemes);
}
}
I have an IdentityServer 4 server set up as a stand-alone app, using net core 3.1, Entity Framework core against MySql and Net Core Identity as a user store. Separately I have a Razor Pages client app, which authenticates against the Identity Server, with user logon taking place on the server. All this is working fine.
I now wish to be able to write a log entry on the client for any new user authentication or failed logon. I assume there must be events raised somewhere. How do I go about this, please?
I had the same requirement, to store for example authentication failure in audit log - if someone tried to access api with invalid token.
What I found is that IdentityServerAuthenticationOptions has JwtBearerEvents property which can be used to be notified about such events.
In my case it worked this way:
.AddAuthentication(AUTH_SCHEME).AddIdentityServerAuthentication(AUTH_SCHEME, options => {
options.ApiName = config.ApiName;
options.Authority = config.Address;
options.JwtBearerEvents = new JwtBearerEvents
{
OnAuthenticationFailed = c =>
{
// Log can bo done here
return Task.CompletedTask;
},
OnTokenValidated = c =>
{
// Log can bo done here
return Task.CompletedTask;
},
OnChallenge = c =>
{
// Log can bo done here
return Task.CompletedTask;
},
OnForbidden = c =>
{
// Log can bo done here
return Task.CompletedTask;
}
};
options.SupportedTokens = SupportedTokens.Jwt;});
I was afraid that it will override normal IS behavior, but everything seems to work as it used to.
You have access to proper contexts there, for example in JwtBearerChallengeContext you can read Error or ErrorDescription strings or even whole HttpContext.
I successfully followed the Deblokt tutorial on IdentityServer4 (https://deblokt.com/2020/01/24/01-identityserver4-quickstart-net-core-3-1/). This tutorial uses the IdentityServer4 QuickstartUI. Userid/password authentication using the AspNet Identity database is working.
I'm using the latest .Net Core (3.1), which differs slightly from the tutorial, but it is working.
Now I'm attempting to add Windows Authentication and hitting a runtime error. What befuddles me is that the error is occurring after the user is successfully authenticated by windows. I'm running locally on a Windows 10 machine inside Visual Studio 2019 in debug mode, using Google Chrome.
Per the IdentityServer4 documentation (https://identityserver4.readthedocs.io/en/latest/topics/windows.html#using-kestrel) I added this to my Startup.cs:
services.Configure<IISServerOptions>(iis =>
{
iis.AuthenticationDisplayName = "Windows";
iis.AutomaticAuthentication = false;
});
I then right-clicked the project, chose "Properties", chose the "Debug" tab, and checked "Windows Authentication":
This makes the "Windows" button visible when I navigate to /Account/Login:
When the "Windows" button is clicked the postback is handled inside the ExternalController "Challenge" method, with "Windows" as the provider argument and "~/" for the returnUrl argument. Since the provider argument is "Windows" this method calls ProcessWindowsLoginAsync, passing the returnUrl argument.
The first time, ProcessWindowsLoginAsync calls HttpContext.AuthenticateAsync("Windows"), which returns a null Principal, which causes the method to re-call /External/Challenge("Windows"), which calls ProcessWindowsLoginAsync a second time, which in turn calls HttpContext.AuthenticateAsync("Windows"), and this 2nd call is successful--HttpContext.AuthenticateAsync returns a windows principal (which is me) and I can view it in the debugger and see that it has all of my particulars correct.
ProcessWindowsLoginAsync then makes this call:
await HttpContext.SignInAsync(
IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme,
new ClaimsPrincipal(id),
props
);
This executes without error. The "id" argument is a ClaimsIdentity with 2 claims pulled from my windows identity, Subject and Name.
At this point all appears to be well. The method ProcessWindowsLoginAsync ends with a Redirect to /External/Callback. This is where the error occurs. The method /External/Callback method contains this bit of code at the top, and is throwing the exception you see. For reasons unknown HttpContext.AuthenticateAsync is not succeeding.
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ExternalScheme);
if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
}
The "result" object has a "Succeeded" value of false and a "None" value of true, the other properties (Principal, Properties, and Ticket) are all null.
Any advice is appreciated.
I had a similar problem, IdentityServer code threw "External authentication error" for Google login
[HttpGet]
public async Task<IActionResult> Callback()
{
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
}
I had to add the external SignInScheme (IdentityServerConstants.ExternalCookieAuthenticationScheme) in my Startup.cs:
services.AddAuthentication()
.AddGoogle(options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = "my_client_id";
options.ClientSecret = "my_client_secret";
});
The fix is to change this, on ExternalController.cs line 173:
await HttpContext.SignInAsync(
IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme,
new ClaimsPrincipal(id),
props);
To this:
await HttpContext.SignInAsync(
IdentityConstants.ExternalScheme,
new ClaimsPrincipal(id),
props);
This seems obvious in hindsight: AuthenticateAsync and SignInAsync should reference the same scheme.
After bumbling across the solution I notice that the IdentityServer4 github already has a closed bug report from March 2018, I don't know why this bug is still part of the Quickstart though: https://github.com/IdentityServer/IdentityServer4/issues/2166
This should be redirecting my app to my AdFs signOut Page, and then redirect me back to my app.
However, it simply redirects me to my route "/logout".
Watching the log on my ADFS server nothing happens.
[AllowAnonymous]
[HttpGet]
[Route("api/logout")]
public async Task<IActionResult> Logout()
{
return SignOut(new AuthenticationProperties()
{
RedirectUri = "/logout"
},
Saml2Defaults.Scheme);
}
SignIn works fine. I even tried this same approach, but does not work. Here, the ReturnUrl method gets the location from HttpContext.Response.Header. When I try this for the logout, the location is always null.
[AllowAnonymous]
[HttpGet]
[Route("api/login")]
public async Task<string> LoginAdfs()
{
string redirectUri = _appSettings.Saml.SpEntityId;
await HttpContext.ChallengeAsync(new AuthenticationProperties
{
RedirectUri = string.Concat(redirectUri, "/autenticado")
});
return ReturnUrl();
}
Any idea what could be happening?
UPDATE 21/11/2019
Turns out the Saml2Handler is simply not trying to send the request to the server. I'm getting these messages on my output window:
Sustainsys.Saml2.AspNetCore2.Saml2Handler: Debug: Initiating logout, checking requirements for federated logout
Issuer of LogoutNameIdentifier claim (should be Idp entity id):
Issuer is a known Idp: False
Session index claim (should have a value):
Idp has SingleLogoutServiceUrl:
There is a signingCertificate in SPOptions: True
Idp configured to DisableOutboundLogoutRequests (should be false):
Sustainsys.Saml2.AspNetCore2.Saml2Handler: Information: Federated logout not possible, redirecting to post-logout
Here is my StartUp Configuration, I don't get what is wrong here:
ServiceCertificate se = new ServiceCertificate()
{
Certificate = new X509Certificate2(SpCert, "",X509KeyStorageFlags.MachineKeySet),
Use = CertificateUse.Signing
};
SPOptions sp = new SPOptions
{
AuthenticateRequestSigningBehavior = SigningBehavior.Never,
EntityId = new EntityId(SpEntityId),
ReturnUrl = new Uri("/login"),
NameIdPolicy = new Sustainsys.Saml2.Saml2P.Saml2NameIdPolicy(null, Sustainsys.Saml2.Saml2P.NameIdFormat.Unspecified),
};
sp.ServiceCertificates.Add(se);
IdentityProvider idp = new IdentityProvider(new EntityId(appSettings.Saml.EntityId), sp);
idp.Binding = Saml2BindingType.HttpPost;
idp.AllowUnsolicitedAuthnResponse = true;
//idp.WantAuthnRequestsSigned = true;
idp.SingleSignOnServiceUrl = new Uri("/login");
//idp.LoadMetadata = true;
idp.SigningKeys.AddConfiguredKey(new X509Certificate2(IdpCert));
idp.MetadataLocation = theMetadata;
idp.DisableOutboundLogoutRequests = true;
For the logout to work, two special claims "LogoutNameIdentifier" and "SessionIndex" (full names are http://Sustainsys.se/Saml2/LogoutNameIdentifier and http://Sustainsys.se/Saml2/SessionIndex need to be present on the user. Those carries information about the current session that the Saml2 library needs to be able to do a logout.
Now I don't see your entire Startup, so I cannot understand your application's flow. But those claims should be present in the identity returned by the library - possibly stored in an External cookie (if you are using asp.net identity). When your application then sets the application cookie those two claims must be carried over to the session identity.
Also you have actually disabled outbound logout with DisableOutboundLogoutRequests. But that's not the main problem here as your logs indicates that the required claims are not present.
From my own experience, the two claims, as mentioned by Anders Abel, should be present on the user. I had not seen these claims until I passed all of the claims along with the sign-in request. ASP.NET Core recreates the principal on SignInAsync and needs claims to be passed in with the request.
With the following, I am able to fulfill a SingleLogout with my service:
await HttpContext.SignInAsync(user.SubjectId, user.Username, props, user.Claims.ToArray());
what you are using as a service provider.
try
{
UserCredential credential;
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
new ClientSecrets { ClientId = ClientID, ClientSecret = ClientSecret },
new[] { AnalyticsService.Scope.AnalyticsReadonly, AnalyticsService.Scope.AnalyticsEdit },
"user",
CancellationToken.None,
new FileDataStore("Analytics.Auth.Store")).Result;
return credential;
}
catch { return null; }
I am using above code for google console web application(Google Analytic) but it gives redirect_uri mismatch error. How i can send redirect_uri.
redirect_uri is set up in the Google Developers console -> apis & auth -> credentials
Not sure if Sanaan C ever found an answer ... the reason that your code does not work in a web application is likely because the user that created the Analytics.Auth.Store entry in that user's %APPDATA% folder is NOT the one running your web application.
Does anyone have a solution to this - and please excuse that this question is appended to another ... I actually think this was the intended question in any event ...
===
One simple-minded solution could be to take the credentials created by a user who can respond to the redirect and put it in a folder, with appropriate access permissions, where the user under which the IIS service is being run can find it. Instantiate the FileDataStore with a full path to this folder ...