Asp.net identity - Reset cookies and session on iis recycle (restart) - c#

I have implemented asp.net mvc with asp.net identity authentication.
I have used cookie based authentication. After restart the IIS/stop and start the IIS for the my site, when i open my website, the user is automatically login to the system.
The user cookie is not cleared and still valid for the user. How to force the user to log out after restart the iis?
I have used default sample from the website.
http://www.nuget.org/packages/Microsoft.AspNet.Identity.Samples
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});

Cookies are not meant to be invalidated when IIS is restarted - this is not how HTTP protocol works. Cookies being invalidated on IIS restart can lead to some strange behaviour in production - IIS can be plunged any time, or there could be load-balancer that uses a few IIS servers to serve the requests - what would happen if one of the servers restarts?
Anyway, you can kill all the cookies for all the users by mass-updating ApplicationUser.SecurityStamp in the database. And in Startup.Auth.cs set validateInterval: TimeSpan.FromMinutes(2) - this will invalidate all the cookies within 2 minutes of the SecurityStamp update. Value lower than this is not recommended - this will cause performance issues.

For this, I have done a trick.
We are using session to store the dynamic variables and asp.net identity for authentication in ASP.NET MVC.
Each request I have interrupted.
I have checked like whether asp.net identity is valid and session is invalid.
If session is invalid, then make the cookies invalid and navigate the user to specific page.
public class SessionHandler : ActionFilterAttribute
{
private ApplicationUserManager _userManager;
private IAuthenticationManager AuthenticationManager
{
get
{
return HttpContext.Current.GetOwinContext().Authentication;
}
}
public ApplicationUserManager UserManager
{
get
{
return _userManager ?? HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
private set
{
_userManager = value;
}
}
public IIdentity UserIdentity
{
get { return System.Web.HttpContext.Current.User.Identity; }
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!string.IsNullOrWhiteSpace(UserIdentity.GetUserId()))
{
if (System.Web.HttpContext.Current.Session["Username"] == null)
{
AuthenticationManager.SignOut();
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary
{
{ "action", "Index" },
{ "controller", "Home" }
});
}
}
}
}
In Global.asax file
Add the following code
GlobalFilters.Filters.Add(new SessionHandler());

Related

Azure Active Directory Integration with WebForms Getting Infinite Loop at Login

I have read and followed this article to setup my site using our AAD (Azure Active Directory) to get SSO (Single Sign On.) I have gotten it to work in a brand new website both with localhost as well as when I publish it to Azure.
Here are the settings for the working version's App Registration:
Branding:
Home page URL: https://<worksgood>.azurewebsites.net
Authentication:
Redirect URIs:
https://localhost:44390/
https://<worksgood>.azurewebsites.net/.auth/login/aad/callback
Implicit grant:
ID Tokens: Checked
Supported account types
Accounts in this organizational directory only (My Company - Single tenant)
Treat application as a public client
No
And when I run the application here is the callback request.
As you can see the Response Header | Location looks good (to me)
Here are the App Registration settings for the site I am attempting to integrate this same logic into:
Branding:
Home page URL: https://<notsogood>.azurewebsites.net
Authentication:
Redirect URIs:
https://localhost:54449/
https://<notsogood>.azurewebsites.net/.auth/login/aad/callback
Implicit grant:
ID Tokens: Checked
Supported account types
Accounts in this organizational directory only (My Company - Single tenant)
Treat application as a public client
No
And when I run the application here is the callback request.
When I run it, I do get the AD login screen where I enter my AD user and creds. However, it does not successfully log me in.
As you can see, the Location in the response gets altered. I do know that this non-working version has the authentication and authorization sections within the web.config and if I change the loginUrl attribute from /login to /loginklg it will change the location to /loginklg?ReturnUrl=%2f.auth%2flogin%2faad%2fcallback but if I remove that section the site will not work.
You should also notice that there is a loop where it attempts to log me in and then for some reason can not and then tries again.
Initially, the not working site had the following startup code for authentication:
public void ConfigureAuth(IAppBuilder app) {
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions {
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/login"),
Provider = new CookieAuthenticationProvider {
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(15),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)
)
}
});
}
I have kept this in as well as taken it out and it makes no difference in my result.
The only real difference is that the working version is MVC and the SignIn method is called.
public void SignIn() {
if (!Request.IsAuthenticated) {
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties { RedirectUri = "/" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
}
And with the not working version it is a WebForm/Page and the Page_Load method is called:
Please Note
This application was not created by me nor my company, so I am trying to integrate it with simply some separate classes and config settings with the least code change as possible. The _avantiaSSOEnabled just reads an appSettings in the web.config which I added. The _openIdEnabled already existed.
_openIdEnabled = false
_avantiaSSOEnabled = true
Even if I enable _openIdEnabled the Location is still bad.
protected void Page_Load(object sender, EventArgs e) {
if (_avantiaSSOEnabled) {
if (!Request.IsAuthenticated) {
Request.GetOwinContext().Authentication.Challenge(
new Microsoft.Owin.Security.AuthenticationProperties { RedirectUri = "/klg" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
}
if (_openIdEnabled)
openIdBackgroundSignIn.OnOpenIdSSOLoggedIn += OnOpenIdSSOLoggedIn;
if (!IsPostBack) {
if (SystemHub.Maintenance.IsActive)
HandleInfoPopup(MaintenenceException.Text, true);
else if (Request["error"] != null)
HandleError(Request["error"].ToString());
else if (Request["auto"] == "true")
HandleInfoPopup(AutoLogout.Text, true);
else if (_openIdEnabled) {
openIdBackgroundSignIn.ClearData();
if (Request["oidc_error"] != null) //This is usually when auto-login fails, so we pass it to client side which will handle it
openIdBackgroundSignIn.AddData(OpenIdBackgroundSignIn.OPENID_KEY_ERROR, Request["oidc_error"].ToString());
else if (Request["oidc_login"] == "true")
openIdBackgroundSignIn.AddData(OpenIdBackgroundSignIn.OPENID_KEY_LOGIN_SUCCESS, true);
else if (User.Identity.IsAuthenticated)
Response.RedirectToUrl(Request.QueryString["ReturnUrl"]);
else if (Request["lo"] == null) //lo is set when coming from logout, so don't try to autologin
openIdBackgroundSignIn.AddData(OpenIdBackgroundSignIn.OPENID_KEY_ATTEMPT_LOGIN_AUTO, true);
}
else if (User.Identity.IsAuthenticated) {
Response.RedirectToUrl(Request.QueryString["ReturnUrl"]);
}
}
}
The only code change I made (that wasn't in the above linked article) was in a attempt to fix it, reading many other articles and there is a known issue with default cookie manager. Here is the result:
app.UseCookieAuthentication(new CookieAuthenticationOptions {
CookieManager = new SystemWebChunkingCookieManager() // Originally SystemWebCookieManager
});
I know I am close. Clearly something is intercepting the request and tweaking it. I am just not sure where to look. I have been coding in C# since the start, but I am not that used to the security/SSO side of it, so any help is appreciated. If you need me to add more information, I can, just let me know what.
UPDATE - 07/31/2020
I was able to fix the Location /login?ReturnUrl... after following this article as you can see below.
As you can see in the image below, from the AAD Sign-in Log, I am successfully logging in. So it seems as if the code is unable to remember or store the token once logged in and then just tries again and must have some threshold of tries or time before it fails.
Oddly enough, when it stops looping I get the following message which has the email of the account I am attempting to log in as and says "Signed in"
The looping issues was fixed by removing the following line:
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
The looping was happening because the authentication type (set in the above line) would return Cookies. However, the response from AAD was setting the type as ApplicationCookie.
The full code in the ConfigAuth is now:
public void ConfigAuth(IAppBuilder app) {
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions {
CookieManager = new SystemWebChunkingCookieManager(),
Provider = new CookieAuthenticationProvider {
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: AuthenticationHelper.OpenIdEnabled
? TimeSpan.FromSeconds(30)
: TimeSpan.FromMinutes(15),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions {
ClientId = AvantiaSSOHelper.ClientId,
Authority = AvantiaSSOHelper.Authority,
PostLogoutRedirectUri = AvantiaSSOHelper.PostLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications {
AuthenticationFailed = (context) => {
context.HandleResponse();
context.Response.Redirect("/?errormessage=" + context.Exception.Message);
return Task.FromResult(0);
},
AuthorizationCodeReceived = (context) => {
Debug.WriteLine($"Authorization code received: {context.Code}");
return Task.FromResult(0);
},
MessageReceived = (context) => {
Debug.WriteLine($"Message received: {context.Response.StatusCode}");
return Task.FromResult(0);
},
SecurityTokenReceived = (context) => {
Debug.WriteLine($"Security token received: {context.ProtocolMessage.IdToken}");
string test = context.ProtocolMessage.AccessToken;
return Task.FromResult(0);
},
SecurityTokenValidated = (context) => {
Debug.WriteLine($"Security token validated: {context.Response.StatusCode}");
var nameClaim = context.AuthenticationTicket.Identity.Claims
.Where(x => x.Type == AvantiaSSOHelper.ClaimTypeWithEmail)
.FirstOrDefault();
if (nameClaim != null)
context.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypes.Name, nameClaim.Value));
return Task.FromResult(0);
},
TokenResponseReceived = (context) => {
string test = context.ProtocolMessage.AccessToken;
return Task.FromResult(0);
}
}
}
);
// This makes any middleware defined above this line run before the Authorization rule is applied in web.config
app.UseStageMarker(PipelineStage.Authenticate);
}
That made a single (non-looping) call and then the system attempted to continue in an Authenticated mode. However, there was still one more step I needed to do. This last step was to alter the SecurityTokenValidated event by adding the appropriate response claim into the authentication ticket. Our system is using Micrososft Identity and is thus based on an email address. So I need to add a Claim of type ClaimTypes.Name to the authentication ticket from the extracted email claims value as follows:
context.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypes.Name, nameClaim.Value));
The AvantiaSSOHelper.ClaimTypeWithEmail is simply a value I am reading out of the Web.config file in case other implementations have a different claim I would need to extsract.

ASP.NET Identity - RedirectLocation is null inside Application_EndRequest

I created a brand new ASP.NET MVC project with Identity Authentication.
The autogenerated Startup.Auth.cs has the following code:
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
}
});
// ......
The important part above is the LoginPath which is set to /Account/Login (which is the default).
When I try to access a page/action with the [Authorize] attribute without being logged in, I'm redirected to that login page, as exptected.
In my Global.asax.cs file, I put the following code:
protected void Application_EndRequest(object sender, EventArgs e)
{
var subnet = ConfigurationManager.AppSettings["SubnetForWindowsAuthentication"].ToString().Split(';');
if (this.Response.StatusCode == 302
&& this.Response.RedirectLocation.Contains("/Account/Login")
&& subnet.Any(ip => this.Request.UserHostAddress.Contains(ip))
&& this.Request.Browser.Win32)
{
this.Response.StatusCode = 401; // ask the browser to negotiate an identity
this.Response.SuppressFormsAuthenticationRedirect = true;
}
}
When the user is being redirected, the StatusCode is 302. Now, I need to check if the user is being redirected to the login page. The problem is this.Response.RedirectLocation is null, even though the response status code is 302.
Why is this.Response.RedirectLocatio null?
Further Details
The funny thing is that this.Response.Headers["location"] has the URL I'm being redirected to (which should be this.Response.RedirectLocation as well). I could use the header directly to check if the URL contains /Account/Login, but when I change this.Response.StatusCode to 401, which should challenge the browser to negotiate a windows authentication identity, for example, but instead it just fails showing an IIS error page displaying: HTTP Error 401.0 - Unauthorized.
I'm trying to achieve the solution proposed in this link, but instead of using Forms Authentication, I'm using ASP.NET Identity.

Basic Authentication to Password Protect Entire Website

For beta testing, I would like to use Basic Authentication (or Digest) but I am unable to combine the Cookie Authentication provided by default in MVC with Basic Authentication.
Following Dead simple ASP.NET MVC 5 password protection? (and older posts such as link1 and link2), I set up an action filter in FilterConfig.cs:
GlobalFilters.Filters.Add(new BasicAuthenticationAttribute("myUsername", "myPassword"));
However, the result is an infinite login loop:
http://localhost:52200/account/login?ReturnUrl=%2Faccount%2Flogin%3FReturnUrl%3D%252Faccount%252Flogin%253FReturnUrl%253D%25252Faccount%25252Flogin%25253FReturnUrl%25253D%2525252Faccount%2525252Flogin%2525253FReturnUrl%2525253D%252525252Faccount%252525252Flogin%252525253FReturnUrl%252525253D%25252525252Faccount%25252525252Flogin%25252525253FReturnUrl%25252525253D%2525252525252Faccount%2525252525252Flogin%2525252525253FReturnUrl%2525252525253D%252525252525252Faccount%252525252525252Flogin%252525252525253FReturnUrl%252525252525253D%25252525252525252Faccount%25252525252525252Flogin%25252525252525253FReturnUrl%25252525252525253D%2525252525252525252F
The project has Anonymous Authentication Enabled and Windows Authentication Disabled. The BasicAuthentication filter is as follows:
using System;
using System.Web;
using System.Web.Mvc;
public class BasicAuthenticationAttribute : ActionFilterAttribute
{
public string BasicRealm { get; set; }
protected string Username { get; set; }
protected string Password { get; set; }
public BasicAuthenticationAttribute(string username, string password)
{
Username = username;
Password = password;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var req = filterContext.HttpContext.Request;
var auth = req.Headers["Authorization"];
if (!string.IsNullOrEmpty(auth))
{
var cred = System.Text.Encoding.ASCII.GetString(Convert.FromBase64String(auth.Substring(6))).Split(':');
var user = new { Name = cred[0], Pass = cred[1] };
if (user.Name == Username && user.Pass == Password)
{
return;
}
else
{
throw new HttpException(403, "Forbidden"); // For Elmah
}
}
var res = filterContext.HttpContext.Response; // The 4 lines below cause the login redirect
res.StatusCode = 401;
res.AddHeader("WWW-Authenticate", string.Format("Basic realm=\"{0}\"", BasicRealm ?? "MyProject"));
res.End();
}
}
My understanding is that upon triggering the http 401, UseCookieAuthentication in Startup.Auth.cs redirects to login, which in turns calls BasicAuthentication, thus starting the loop until the browser throws an error. This is the default UseCookieAuthentication:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
Many posts indicate that web.config needs to modified (Owin, formsAuthentication, etc.) I won't cite them here because there seems to be no commonly accepted answer.
Would a temporary password-protected index be wiser / preferable for beta testing?
Per OWIN Authentication with IIS Basic Authentication, the following line in Startup.Auth.cs must be deleted to avoid starting an infinite login loop:
LoginPath = new PathString("/Account/Login"),
It is the only way for Cookie Authentication and Basic Authentication to work simultaneously in MVC - no further setup required in IIS. Once a user has been authenticated via Basic Authentication, users who have logged in remain successfully logged in across the site.

How to remove access to site when the user had a previously valid cookie?

A user had access to a .net MVC OWIN authenticated website and has a cookie that logs them in. The administrator then revokes access and resets their role to a No Access role.
How can the site detect this? Can the Cookie authentication code be updated to check only the Roles for the user in the database upon logging in? If so, can someone point me in the right direction to override this function?
UPDATE:
I beleive that the code below in the OWIN Startup class is what needs to be updated to intercept/override the cookie authenticaiton. What I am trying to do is to check if the user has access to the site (if the user is part of a No Access role, deny them the login).
public partial class Startup
{
public void ConfigureAuthentication(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = context =>
{
//Somehow check the users role here?
}
}
});
}
}
Here is what I ended up doing which appears to work the way I think it should work. When the OWIN cookie is read, I check the users database (I realize that this is redundant) to ensure that their access has not been revoked. If it has been revoked, I reject the cookie authentication and they are sent back to the site login screen.
public partial class Startup
{
public void ConfigureAuthentication(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Login"),
Provider = new CookieAuthenticationProvider()
{
OnValidateIdentity = context =>
{
string userName = context.Identity.Name;
User user = userRepository.Users.SingleOrDefault(u => u.UserName == userName);
if(user.Roles.SingleOrDefault(r => r.Name == "No Access") != null)
{
context.RejectIdentity();
}
return System.Threading.Tasks.Task.FromResult(0);
}
}
});
}
}
From my testing, this appears to work. I think there is probably a better, less redundant solution to this, but I haven't come across it. I believe that this code runs with every page load which is what worries me a bit. I'll probably post another question later asking if there is a better way to do this, but for now, this works.
Mark C's response really helped me tailor my search on this subject, thanks.
The simplest solution is to "delete" the user's cookies so that they are logged out. Then when they log in again, they'll have the new permissions.
I say "delete", because you don't actually delete the cookie, but make it invalid - typically by setting it's date in the past.
This code from the MSDN will set the expiry date of the cookie to one day in the past:
if (Request.Cookies["UserSettings"] != null)
{
HttpCookie myCookie = new HttpCookie("UserSettings");
myCookie.Expires = DateTime.Now.AddDays(-1d);
Response.Cookies.Add(myCookie);
}
Source

c# owin can't get User.Identity after signin

After i finish my login code
var identity = new ClaimsIdentity(claims, OAuthConfigur.AuthenticationType);
this.AuthenticationManager.SignIn(new AuthenticationProperties
{
ExpiresUtc = DateTimeOffset.Now.AddMinutes(30),
IsPersistent = false
}, identity);
return RedirectToAction("Index", "Home");
After RedirectToAction , there is the cookie in broswer.
But when Authorize attribute there is no Authorize.
In my custom Authorize actionfilter ,
httpContext.User.Identity.IsAuthenticated
always return false.
I find a way to get identity below:
private ClaimsIdentity GetIdentity(HttpContextBase httpContext)
{
var ticket = httpContext.GetOwinContext().Authentication
.AuthenticateAsync(OAuthConfigur.AuthenticationType).Result;
var identity = ticket != null ? ticket.Identity : null;
return identity;
}
after this function, i can get the useridenttity.
Is this correct??
If i need users login info , i need call this function everytime is action?
Thank you reply!
Here's my Startup.cs
public void ConfigureAuth(IAppBuilder app)
{
// Enable Application Sign In Cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = OAuthConfigur.AuthenticationType,
AuthenticationMode = AuthenticationMode.Passive,
LoginPath = new PathString(OAuthPaths.LoginPath),
LogoutPath = new PathString(OAuthPaths.LogoutPath),
ExpireTimeSpan = TimeSpan.FromMinutes(20)
});
// Setup Authorization Server
app.UseOAuthAuthorizationServer(new CustomerOAuthAuthorizationServerOptions());
}
Just in case someone stumbles upon this in the future. I had the same issue and I was pulling my hair out when I realised that I had set the
CookieSecure = CookieSecureOption.Always
on the CookieAuthenticationOptions class :/
So obviously cookies were only access over https and because my local environment was not setup with https (It used to be) it could not read the cookie.
I have one scenario when published the application to Production server the call httpContext.GetOwinContext().Authentication
.AuthenticateAsync("Application") always return null in IE browser. For this case, go to IE browser Internet Options -> Trusted sites, add your identity server application url as trusted site. System works then.

Categories