I am trying to use Windows Authentication credentials to connect with my native (Winforms, console app) client to Identity Server hosted on IIS. The point is for user to be authenticated by AD and with those credentials get the right claims and roles from the Identity Server (which is run through commercial https://commercial.abp.io/ platform).
EDIT:
I found out it is not client related issue since i cannot use my External login (Windows credentials) even directly on hosted site.
The thing worked locally while hosted by IISExpress, then i published it to IIS and enabled the Anonymous and Windows Authentication in the IIS settings and here is where problems began.
When i run it and click the External Login (Windows Credentials) button i usually get a redirect to https://myserver/Error?httpStatusCode=401
and i get prompt for my windows credentials (which even if i insert correctly, just repeat prompt again).
From time to time i get logged in with my Windows credentials (which is the goal). Login with username and password works fine.
I saw the similar issue mentioned by someone here:
https://github.com/IdentityServer/IdentityServer4/issues/4937 without any solution\answer.
My client is basically the sample NativeConsolePKCEClient from this https://github.com/damienbod/AspNetCoreWindowsAuth
static string _authority = "https://myserver/";
string redirectUri = "https://127.0.0.1:45656";
var options = new OidcClientOptions
{
Authority = _authority,
ClientId = "native.code",
ClientSecret = "secret",
RedirectUri = redirectUri,
Scope = "openid profile",
FilterClaims = false,
Browser = browser,
Flow = OidcClientOptions.AuthenticationFlow.AuthorizationCode,
ResponseMode = OidcClientOptions.AuthorizeResponseMode.Redirect,
LoadProfile = true
};
_oidcClient = new OidcClient(options);
var result = await _oidcClient.LoginAsync();
and on server side the startup configuration services:
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.Configure<IISOptions>(iis => // IISOptions
{
iis.AuthenticationDisplayName = "Windows";
iis.AutomaticAuthentication = false;
});
context.Services.AddAuthentication()
.AddJwtBearer(options =>
{
options.Authority = configuration["AuthServer:Authority"];
options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]); ;
options.Audience = "ABPIdentityServer";
});
}
Here is the ProcessWindowsLoginAsync challenge method:
private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl)
{
// see if windows auth has already been requested and succeeded
var result = await HttpContext.AuthenticateAsync(Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme);
if (result?.Principal is WindowsPrincipal wp)
{
// we will issue the external cookie and then redirect the
// user back to the external callback, in essence, tresting windows
// auth the same as any other external authentication mechanism
var props = new AuthenticationProperties()
{
RedirectUri = "./ExternalLoginCallback",
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme },
}
};
var id = new ClaimsIdentity(Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme);
id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name));
id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));
// add the groups as claims -- be careful if the number of groups is too large
{
var wi = (WindowsIdentity)wp.Identity;
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
{
// trigger windows auth
// since windows auth don't support the redirect uri,
// this URL is re-triggered when we call challenge
return Challenge("Windows");
}
}
I am suspecting that this piece of code when calling Challenge somehow returns up redirecting to error page, but i am not sure and i do now why.
So what am i missing? Is it even possible to run both Windows and Anonymous authentication on IIS?
Here i also found similar issue:
identity server 4 windows authentication
but the presented answers did not help me.
I strongly suspect that it's not the client issue it's the token provider's issue (Not the ID4 library but one where you have installed the ID4 library).
I believe that you have added the below code in the AccountController->Login action but make sure that you have added a success check in it, if you miss that then your app will go infinite loop.
[HttpGet] public async Task<IActionResult> Login(string returnUrl)
{
if(loginViewModel.ExternalLoginScheme == "Windows")
{
var authenticationResult = await HttpContext.AuthenticateAsync("Windows").ConfigureAwait(false);
if (authenticationResult.Succeeded && authenticationResult?.Principal is WindowsPrincipal windowsPrinciple)
{
// Add your custom code here
var authProps = new AuthenticationProperties()
{
RedirectUri = Url.Action("Callback"),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", "Windows"},
}
};
await HttpContext.SignInAsync();
return Redirect(RedirectUri);
}
else
{
return Challenge("Windows");
}
}
}
I hope this will help you to fix your issue.
Happy Coding!!
Just for anybody who might be interested. I found out what was causing the redirect error.
It is somehow connected with the ABP Suite i used for generating the base application.
there in the ApplicationInitialization there was a middleware called
app.UseErrorPage();
Which when the Windows credentials were challenged took it as an Error and redirected to https://myserver/Error?httpStatusCode=401.
I am not sure how this middleware works and why sometimes login worked, but removing this part solved my issue.
I hope this helps somebody, somehow, sometime..
Related
I have enabled authentication for azure ad by following some sample apps from MS as shown below. I then use the authorize statements below as well so my application should be locked down unless the user is authenticated. My issue in my testing environment is if i restart IIS express then when it starts back up I am not required to log back in if the browser is still open. I can openly navigate my application but when any function related to MS Graph is called it fails with error: ErrorCode: user_null Microsoft.Identity.Client.MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call. So its as if my application thinks I am still authorized but really I am not because the MS Graph api call fails because of token related issues. If I force myself to logout then log back in everything works. For my live site if a user is inactive for some time but the browser is left open this same issue occurs and they are not forced to relogin so these issues again can happen until they force logout and re log back in. What have I setup wrong or need to add to force relogin before these issues arise? Or can I keep the token working without forcing the user to relogin?
// Add services to the container.
builder.Services.AddRazorPages().AddRazorPagesOptions(options =>
{
options.Conventions.AllowAnonymousToFolder("/Login");
options.Conventions.AuthorizeFolder("/");
options.Conventions.AuthorizeFolder("/files");
});
//authentication pipline
builder.Services.AddHttpContextAccessor();
var initialScopes = builder.Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
builder.Configuration.Bind("AzureAd", options);
options.Events = new OpenIdConnectEvents
{
//Tap into this event to add a UserID Claim to a new HttpContext identity
OnTokenValidated = context =>
{
//This query returns the UserID from the DB by sending the email address in the claim from Azure AD
string query = "select dbo.A2F_0013_ReturnUserIDForEmail(#Email) as UserID";
string connectionString = builder.Configuration.GetValue<string>("ConnectionStrings:DBContext");
string signInEmailAddress = context.Principal.FindFirstValue("preferred_username");
using (var connection = new SqlConnection(connectionString))
{
var queryResult = connection.QueryFirst(query, new { Email = signInEmailAddress });
var claims = new List<Claim>
{
new Claim("UserID", queryResult.UserID.ToString())
};
var appIdentity = new ClaimsIdentity(claims);
context.Principal.AddIdentity(appIdentity);
}
return Task.CompletedTask;
},
};
})
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
//Add Transient Services
builder.Services.AddTransient<IOneDrive, OneDrive>();
builder.Services.AddControllers(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
builder.Services.AddRazorPages()
.AddMicrosoftIdentityUI();
Then in all of my controllers I use:
[AuthorizeForScopes(ScopeKeySection = "DownstreamApi:Scopes")]
Adding
var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(initialScopes);
before my graph api call fixes my issue. However, I feel as though I shouldnt need to call this everytime but only if the token expires. So is there a way to check if the token expires?
There are two things that worked for me. One was trying to get a token manually if an error occurred using the following:
var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(initialScopes);
The other solution that seemed to work the same was to add the following catch as well:
try
{
Console.WriteLine($"{svcex}");
string claimChallenge = WwwAuthenticateParameters.GetClaimChallengeFromResponseHeaders(svcex.ResponseHeaders);
_consentHandler.ChallengeUser(initialScopes, claimChallenge);
}
catch (Exception ex2)
{
_consentHandler.HandleException(ex2);
}
I do not know enough about what consent handler does to provide more info on why this works but will update my answer once I do.
Using IIS Express on my local machine, I'm able to run the IdentityServer4 QuickStart UI project and successfully sign in. However, once it is deployed to production, I'm unable to get it to work.
On the Application Pool for the site, I have a domain account setup (with just about every permission possible granted). I have tried every variation of having "anonymous authentication" toggled. I've gone as far as recreating the entire application from scratch in multiple different ways (no-SSL, only-SSL, fully open CORS, all security policies disabled), and even the most basic version of the application seems to suffer from the exact same issue.
After slapping some logging on the application, I can see that I'm grabbing the Subject ID and name from AD just fine.
Here's the ProcessWindowsLoginAsync method, with only minimal logging changes.
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("Callback"),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", AccountOptions.WindowsAuthenticationSchemeName },
}
};
var id = new ClaimsIdentity(AccountOptions.WindowsAuthenticationSchemeName);
var sub = wp.FindFirst(ClaimTypes.PrimarySid).Value;
id.AddClaim(new Claim(JwtClaimTypes.Subject, sub));
id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));
_logger.LogInformation("Assigning claims. Subject {#Subject}. Name {#Name}", sub, 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(
IdentityServerConstants.ExternalCookieAuthenticationScheme,
new ClaimsPrincipal(id),
props);
return Redirect(props.RedirectUri);
}
return Challenge(AccountOptions.WindowsAuthenticationSchemeName);
}
The above code spits out something akin to (with identifying information stripped):
Assigning claims. Subject S-0-0-00-0000000000-0000000000-0000000000-00000. Name DOMAIN\NAME
Once the above has executed, the external callback method is called and it immediately throws an exception:
[HttpGet]
public async Task<IActionResult> Callback()
{
var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
_logger.LogInformation("We were not successfully able to sign in. Failure: {#Failure}. None: {#None}", result?.Failure, result?.None);
if (result?.Failure != null)
throw result.Failure;
throw new Exception("External authentication error");
}
if (_logger.IsEnabled(LogLevel.Debug))
{
var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}");
_logger.LogDebug("External claims: {#claims}", externalClaims);
}
var (user, provider, providerUserId, claims) = FindUserFromExternalProvider(result);
if (user == null)
user = AutoProvisionUser(provider, providerUserId, claims);
var additionalLocalClaims = new List<Claim>();
var localSignInProps = new AuthenticationProperties();
ProcessLoginCallbackForOidc(result, additionalLocalClaims, localSignInProps);
var issuer = new IdentityServerUser(user.SubjectId)
{
DisplayName = user.Username,
IdentityProvider = provider,
AdditionalClaims = additionalLocalClaims
};
await HttpContext.SignInAsync(issuer, localSignInProps);
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username, true, context?.ClientId));
if (context != null)
if (await _clientStore.IsPkceClientAsync(context.ClientId))
return this.LoadingPage("Redirect", returnUrl);
return Redirect(returnUrl);
}
From the logs, I can tell that it's immediately failing after attempting to authenticate. There's no other errors, but a few interesting logs of note (in order):
Performing protect operation to key {xxxxxxxx-xxxx-xxxx-xxxx-b7e4d6dd250a} with purposes ('C:\websites\identity.ourdomain.com', 'Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware', 'idsrv.external', 'v2').
AuthenticationScheme: idsrv.external signed in.
Executing RedirectResult, redirecting to /External/Callback
Executing action method IdentityServer4.Quickstart.UI.ExternalController.Callback (Idsvr.Api) - Validation state: Valid
AuthenticationScheme: idsrv.external was not authenticated.
(Exception)
One of the possible root cause is that the callback cookie doesn't set properly.
Try to capture the network traffic, and check if idsrv.external cookie has been set correctly during Challenge.
In my case, setting cookie failed because SameSite=None is there without Secure=true.
I have a requirement where I want to fetch the Authorization Code for Azure login for each request in web API. As of now once the user signs in to Azure, after that I am not getting the authorization code as the user is already signed in.
How can I force the user to sign in again? This is the code I have been using as of now in the owin_startup file in web API?
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
CookieSecure = (CookieSecureOption)Convert.ToInt32(cookieSecure), // CookieSecureOption.NeverAlways
CookieManager = new SystemWebCookieManager(),
CookieHttpOnly = false,
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = Authority,
RedirectUri = RedirectUri,
});
According to the code and the cases you post before, I think it it not about Azure ad b2c, so here I will give a reply for azure ad.
When you request an authorization code, there is a prompt=login property which is indicate the user should be prompted to reauthenticate.
Also here is an article about Forcing reauthentication with Azure AD which suggest use Token Max Age to achieve it.
You can append max_age= to the authorization URL (or just put 0 to force password authentication at all times). So once user gets redirected to the URL, they will be presented with an information to login again.
public class RequireReauthenticationAttribute : Attribute, IAsyncResourceFilter
{
private int _timeElapsedSinceLast;
public RequireReauthenticationAttribute(int timeElapsedSinceLast)
{
_timeElapsedSinceLast = timeElapsedSinceLast;
}
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
{
var foundAuthTime = int.TryParse(context.HttpContext.User.FindFirst(AppClaimTypes.AuthTime)?.Value, out int authTime);
if (foundAuthTime && DateTime.UtcNow.ToUnixTimestamp() - authTime < _timeElapsedSinceLast)
{
await next();
}
else
{
var state = new Dictionary<string, string> { { "reauthenticate", "true" } };
await context.HttpContext.Authentication.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties(state)
{
RedirectUri = context.HttpContext.Request.Path
}, ChallengeBehavior.Unauthorized);
}
}
}
It appears that the reason you wish to re-authenticate the user is to get/refresh the token in memory even when the auth cookie is present. Simplest way to achieve this would be decorate your controller with the AuthorizeForScopes attribute. You can check the sample project on Github here
I want to redirect user to the same client after he logged out from that client. So if i have lets say 5 clients on one identity server, i want users to be able to log out from one client and be on that same client but logged out.
The one thing i have tried is to use PostLogoutRedirectUri in AccountController in quickstart, but the value is always null. Workaround that i found is to manually set PostLogoutRedirectUri, that works fine if you have only one client on the server, but not so much if I have multiple. Is there any way to know which client has been "logged out"?
public async Task<IActionResult> Logout(LogoutInputModel model)
{
// build a model so the logged out page knows what to display
var vm = await BuildLoggedOutViewModelAsync(model.LogoutId);
if (User?.Identity.IsAuthenticated == true)
{
// delete local authentication cookie
await HttpContext.SignOutAsync();
// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
}
// check if we need to trigger sign-out at an upstream identity provider
if (vm.TriggerExternalSignout)
{
// build a return URL so the upstream provider will redirect back
// to us after the user has logged out. this allows us to then
// complete our single sign-out processing.
string url = Url.Action("Logout", new { logoutId = vm.LogoutId });
// this triggers a redirect to the external provider for sign-out
return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme);
}
vm.PostLogoutRedirectUri = "http://localhost:56582";
return Redirect(vm.PostLogoutRedirectUri);
}
My Client
new Client
{
ClientId = "openIdConnectClient",
ClientName = "Implicit Client Application Name",
AllowedGrantTypes = GrantTypes.Implicit,
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"role",
"customAPI.write"
},
RedirectUris = new List<string>{ "http://localhost:56582/signin-oidc" },
PostLogoutRedirectUris = new List<string>{ "http://localhost:56582" },
// FrontChannelLogoutUri = "http://localhost:56582/signout-oidc"
}
You are not supposed to set the uri manually. Actually the default logout method from the IdentityServer samples works fine.
When you try the 3_ImplicitFlowAuthentication sample project, you'll see PostLogoutRedirectUri is not null and the redirection works (but not automatically).
The reason why PostLogoutRedirectUri is null in your case is probably because the id_token is not preserved. In MvcClient.Startup make sure you add this line:
options.SaveTokens = true;
That will preserve the tokens in a cookie.
In order to automatically redirect back to the client, make a few adjustments to the sample code. In IdentityServer AccountOptions set
AutomaticRedirectAfterSignOut = true;
In the AccountController.Logout method add the following lines:
if (vm.AutomaticRedirectAfterSignOut &&
!string.IsNullOrWhiteSpace(vm.PostLogoutRedirectUri))
return Redirect(vm.PostLogoutRedirectUri);
Just before the last line:
return View("LoggedOut", vm);
When you run the sample again you should see that the user is now automatically returned to the client after logout.
I am using Identity Server 3 for a central authentication server to a .Net MVC web application I am building.
I have configured the authentication server to use the Open ID Connect identity provider in order to allow users to authenticate against a multi-tenant Azure Active Directory account, using the Hybrid flow.
Currently, sign in works as expected with my client application redirecting to the authentication server which in turn redirects to Microsoft for login before returning back to my client application with a correctly populated Access Token.
However, when I try to logout I am redirected to Microsoft correctly, but the page stops when it arrives back at the authentication server, rather than continuing back to my client application.
I believe I have setup the post logout redirect correctly as outlined here and I think all of my settings are ok.
When I pull the Identity Server 3 code down and debug it, it is correctly setting the signOutMessageId onto the query string, but hits the following error inside the UseAutofacMiddleware method when it is trying to redirect to my mapped signoutcallback location:
Exception thrown: 'System.InvalidOperationException' in mscorlib.dll
Additional information: Headers already sent
My Authentication Server setup:
app.Map("identity", idsrvApp => {
var idSvrFactory = new IdentityServerServiceFactory();
var options = new IdentityServerOptions
{
SiteName = "Site Name",
SigningCertificate = <Certificate>,
Factory = idSvrFactory,
AuthenticationOptions = new AuthenticationOptions
{
IdentityProviders = ConfigureIdentityProviders,
EnablePostSignOutAutoRedirect = true,
PostSignOutAutoRedirectDelay = 3
}
};
idsrvApp.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
idsrvApp.UseIdentityServer(options);
idsrvApp.Map("/signoutcallback", cb => {
cb.Run(async ctx => {
var state = ctx.Request.Cookies["state"];
ctx.Response.Cookies.Append("state", ".", new Microsoft.Owin.CookieOptions { Expires = DateTime.UtcNow.AddYears(-1) });
await ctx.Environment.RenderLoggedOutViewAsync(state);
});
});
});
My Open Id Connect setup to connect to Azure AD:
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "aad",
SignInAsAuthenticationType = signInAsType,
Authority = "https://login.microsoftonline.com/common/",
ClientId = <Client ID>,
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = new TokenValidationParameters
{
AuthenticationType = Constants.ExternalAuthenticationType,
ValidateIssuer = false,
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(<Client ID>, <Client Secret>);
string tenantId = context.AuthenticationTicket.Identity.FindFirst("tid").Value;
AuthenticationContext authContext = new AuthenticationContext($"https://login.microsoftonline.com/{tenantId}");
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
code, new Uri(<Identity Server URI>/aad/"), credential, "https://graph.windows.net");
return Task.FromResult(0);
},
RedirectToIdentityProvider = (context) =>
{
string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
context.ProtocolMessage.RedirectUri = appBaseUrl + "/aad/";
context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl + "/signoutcallback";
if (context.ProtocolMessage.RequestType == Microsoft.IdentityModel.Protocols.OpenIdConnectRequestType.LogoutRequest)
{
var signOutMessageId = context.OwinContext.Environment.GetSignOutMessageId();
if (signOutMessageId != null)
{
context.OwinContext.Response.Cookies.Append("state", signOutMessageId);
}
}
return Task.FromResult(0);
}
});
I cannot find any information about the cause of or solution to this problem. How do I configure this to correctly redirect back to my client application?
Edit:
Related discussion on GitHub: https://github.com/IdentityServer/IdentityServer3/issues/2657
I have also tried this with the latest version of Identity Server on MyGet (v2.4.1-build00452) with the same problem.
I have also created a repository that reproduces the issue for me here: https://github.com/Steve887/IdentityServer-Azure/
My Azure AD setup:
I believe you were experiencing a bug that is fixed in 2.5 (not yet released as of today): https://github.com/IdentityServer/IdentityServer3/issues/2678
Using current source from Git, I still see this problem. It appears to me that AuthenticationController.Logout is hit twice during the logout. Once prior to the external provider's logout page is displayed, and once after. The initial call Queues and clears the signout cookie so that the second time it is not available when rendering the logout page.