What exactly does Owin rememberBrowser do? - asp.net-mvc

In several places in a standard ASP.Net MVC Identity 2.0 Owin implementation you'll see rememberBrowser, like:
await signInManager.SignInAsync(user, isPersistent: isPersistent, rememberBrowser: false);
If you do set rememberBrowser to true, I've noticed that I can kill the browser, kill IIS Express, delete the user the browser was logged in as, even restart my machine, and the browser is still treated as logged-in. Not so great, considering a deleted user being treated as authorized/logged-in is going to cause all sorts of issues in code behind the [Authorize] attribute that expects to have a valid user to work with.
So what is it exactly that rememberBrowser is doing, and is there any risk that someone could just fake rememberBrowser in their cookies to bypass OWIN login? It seems the point of [Authorize] is to guarantee no one but logged-in users access a given Controller Action, and rememberBrowser seems to be a hole in that guarantee.
Bonus question: Is there a way to disable rememberBrowser so that even if a forged cookie did come in, it would be rejected?

I think rememberBrowser is relevant only in Two-factor authentication. So if you set it to true, the browser will acquire TwoFactorRememberBrowser cookie which allow the user to skip 2FA authentication (if enabled) during the login process.
Is there a way to disable rememberBrowser so that even if a forged
cookie did come in, it would be rejected?
The cookie created from rememberBrowser is used in conjunction with the authentication cookie. It will only allow the user to skip 2FA, therefore it is useless without being authenticated first.

The answer by #Hezye is correct, but I'll elaborate on this a bit more.
Here is the code that creates an identity for "rememberBrowser" CreateTwoFactorRememberBrowserIdentity (https://aspnetidentity.codeplex.com/SourceControl/latest#src/Microsoft.AspNet.Identity.Owin/Extensions/AuthenticationManagerExtensions.cs line 215):
public static ClaimsIdentity CreateTwoFactorRememberBrowserIdentity(this IAuthenticationManager manager,
string userId)
{
if (manager == null)
{
throw new ArgumentNullException("manager");
}
var rememberBrowserIdentity = new ClaimsIdentity(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
rememberBrowserIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId));
return rememberBrowserIdentity;
}
So this identity is with type of "TwoFactorRememberBrowserCookie" and with only claim of user ID.
Looking on the source code of SignInManager that uses this code: (https://aspnetidentity.codeplex.com/SourceControl/latest#src/Microsoft.AspNet.Identity.Owin/SignInManager.cs line 106) :
if (rememberBrowser)
{
var rememberBrowserIdentity = AuthenticationManager.CreateTwoFactorRememberBrowserIdentity(ConvertIdToString(user.Id));
AuthenticationManager.SignIn(new AuthenticationProperties { IsPersistent = isPersistent }, userIdentity, rememberBrowserIdentity);
}
Here IAuthenticationManager is used to sign-in 2 identities: one for the actual user, another for "rememberBrowser". And I believe this will produce 2 cookies - one user authentication cookie, another remembering the browser.
In SignInManager when using SignInOrTwoFactor the code (line 218) checks if "RememberBrowser" identity is already set in the cookies.
OWIN cookies are protected by encryption, encryption is borrowed from DpapiDataProtector (documentation). I'm no expert in cryptography so can't comment on the strength of cryptography. I'm just saying that "rememberBrowser" cookie is encrypted the same way as the main authentication cookie.
Regarding your exercise where you restarted your IIS, machine, etc. Have you removed the cookies from the browser? Because if you have not, Identity (or rather OWIN) will treat browser as logged-in, even if the original user record is removed from the database. Though user will not be logged-in for long as there is code in the default template MVC that checks with the database for the user record and logs out if user record have been changed.
As for disabling "rememberBrowser" - always pass false to that argument. And the second cookie will not be set.

Related

ASP.NET Core, route is not triggered when defined as OpenId SignedOutCallbackPath

I have this controller
[Route("Authentication")]
public class AuthenticationController : Controller
{
and this action
[HttpGet("SignOut")]
public async Task<IActionResult> SignOut([FromQuery] string sid)
{
await ControllerContext.HttpContext.SignOutAsync(Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults.AuthenticationScheme);
await ControllerContext.HttpContext.SignOutAsync(Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme);
return View();
This works as expected.
But when I configure a SignedOutCallbackPath for my OpenId authentication that has the same route, it doesn't work anymore. The constructor of my controller is not called, the action is not hit and the result in the browser is a blank page (code 200) with html/head/body, but all empty, that doesn't match any template or view.
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.Cookie.HttpOnly = true;
})
.AddOpenIdConnect(options =>
{
options.SignedOutCallbackPath = "/Authentication/SignOut";
Is the SignedOutCallbackPath not supposed to be a view of my own?
The callback paths in the OpenID Connect authentication scheme are internal paths that are used for the authentication flow of the OpenID Connect protocol. There are three of those:
CallbackPath – The path the authentication provider posts back when authenticating.
SignedOutCallbackPath – The path the authentication provider posts back after signing out.
RemoteSignOutPath – The path the authentication provider posts back after signing out remotely by a third-party application.
As you can see from my explanation, these are all URLs that the authentication provider uses: They are part of the authentication flow and not to be directly used by your users. You also don’t need to worry about handling those things. The OpenID Connect authentication handler will automatically respond to these requests when the authentication middleware runs.
This means that when you change a callback path to some path that is a route of one of your controller actions, then the authentication middleware will handle that request before your controller gets involved. This is by design, so that you do not need to worry about these routes as they are mostly internal.
You just have the option to change those paths if you cannot or do not want to use the default values.
Now, there are two possible things I can think of that you could have meant to change instead:
SignedOutRedirectUri: This is the URL the user gets redirected to after the sign-out process is completed. This basically allows you to send the user to some view with e.g. a message “you were successfully signed out”, to show that the sign-out is done.
You can also set this as part of the AuthenticationProperties that you can pass to the SignOutAsync.
CookieAuthenticationOptions.LogoutPath: This is the URL that is configured to the actual URL that users can go to to sign out of the application. This does not really have that much of an effect though.
Otherwise, it’s really up to you to send users to your /Authentication/SignOut URL. You can put a button into your layout that goes there for example, to offer users a sign out functionality at all times.
Your Action is expecting parameter which is not passed by your callback, the parameter is seemingly not used within the action either, so you can either omit the parameter or make it optional
[HttpGet("SignOut")]
public async Task<IActionResult> SignOut()
{
await ControllerContext.HttpContext.SignOutAsync(Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults.AuthenticationScheme);
await ControllerContext.HttpContext.SignOutAsync(Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme);
return View();
}
For Azure AD I found this post logout redirection behavior:
It doens't work for root accounts of the tenant, that is my personal account, which created Azure
subscription.
But it works for new accounts I created inside of my
subscription.

ASP.NET MVC Action Parameters Not Binding when form sits idle

I am experiencing a strange issue with my mvc 5 web application that I can't seem to figure out. The Application is an internal web app I've built for my organization. Primarily it is accessed by employees who use domain connected computers but also will access via mobile devices and tablets. I wanted to provide the former an automatic login experience through windows auth and AD (without having to enter credentials if they were already signed on to the domain) I also wanted to be able to provide all other users with a custom login screen rather than the browser native prompt. To implement this I created a separate web app which authenticates the windows users and sends an encrypted cookie back to the main app with the user's roles. Non windows based browsers are presented with a login page in the main app that authenticates against AD and retrieves the user's roles. For each type of login the roles are than converted to claims and a federated token is created for the user.
My problem is that when a user logs in via the redirect to the windows auth app a strange issue is occuring. Any form that I submit whether it be standard form submit or an AJAX post has to be submitted within a minute of loading the page otherwise the parameters sent to the controller action do not bind (null). If a user logins in via the custom login page this problem doesn't exist.
Here is the code that performs the initial authentication in global.asax:
Protected Sub Application_AuthenticateRequest()
Dim user As System.Security.Principal.IPrincipal = HttpContext.Current.User
If user Is Nothing Then
'First check if an authentication cookie is has been generated from the windows login
'authentication app
Dim authCookie As HttpCookie = Request.Cookies(".ConnectAUTH")
If Not authCookie Is Nothing Then
' Extract the roles from the cookie, and assign to our current principal, which is attached to the HttpContext.
Dim ticket As FormsAuthenticationTicket = FormsAuthentication.Decrypt(authCookie.Value)
Dim claims As New List(Of Claim)
For Each role In ticket.UserData.Split(";"c)
claims.Add(New Claim(ClaimTypes.Role, role))
Next
Dim claimIdent As New ClaimsIdentity(claims, "Custom")
claimIdent.AddClaim(New Claim(ClaimTypes.WindowsAccountName, ticket.Name))
claimIdent.AddClaim(New Claim(ClaimTypes.NameIdentifier, ticket.Name))
Dim claimPrinc As New ClaimsPrincipal(claimIdent)
Dim token = New SessionSecurityToken(claimPrinc)
Dim sam = FederatedAuthentication.SessionAuthenticationModule
sam.WriteSessionTokenToCookie(token)
HttpContext.Current.User = New ClaimsPrincipal(claimIdent)
Return
Else 'User hasn't been authenticated
Dim ConnectBaseURL = Request.Url.GetLeftPart(UriPartial.Authority) & "/"
Dim mvcPath As String = Request.Url.ToString.Replace(ConnectBaseURL, "")
'If user is requesting the login page then let them authenticate through there
If mvcPath.ToUpper.Contains("ACCOUNT/LOGIN") Or mvcPath.ToUpper.Contains("ACCOUNT/LOGUSERIN") Or mvcPath.ToUpper.Contains("SIGNALR") Then Exit Sub
'for brevity i will omit the code below:
' Basically it checks whether the browser is windows based if so then it redirects the user to
' a windows login authenticator which authenticates the user against Active Directory, builds and sends
' an encrypted cookie with a list of roles/groups that the user belongs to. When this function detects that cookie
' it decrypts it, sets up a claims identity, add the user roles and creates a federated authentication token
'If the the browser is not windows based then it redirects the user to a custom login page which is tied to an MVC
'action that will also authenticate the user against active directory and and set up the claims identity ...
End If
End If
End Sub
Here is the code that authenticates the user if they are redirected to the custom login page:
<AllowAnonymous>
<HttpPost>
<ValidateAntiForgeryToken>
<OutputCache(NoStore:=True, Duration:=0, Location:=OutputCacheLocation.None, VaryByParam:="None")>
Public Function LogUserIn(model As User, returnURL As String) As ActionResult
Try
If ModelState.IsValid Then
If Membership.ValidateUser(model.UserName, model.PassWord) Then
'Call helper function to get all user roles and convert them to claims
Dim userClaims As List(Of Claim) = New LDAPHelper().GetUserGroups(model.UserName)
userClaims.Add(New Claim(ClaimTypes.WindowsAccountName, model.UserName))
userClaims.Add(New Claim(ClaimTypes.NameIdentifier, model.UserName))
userClaims.Add(New Claim(ClaimTypes.Name, model.UserName))
Dim claimIdent As New ClaimsIdentity(userClaims, "Custom")
Dim claimPrinc As New ClaimsPrincipal(claimIdent)
Dim token = New SessionSecurityToken(claimPrinc)
Dim sam = FederatedAuthentication.SessionAuthenticationModule
sam.WriteSessionTokenToCookie(token)
If returnURL Is Nothing Then
Return Redirect("~/")
Else
Return Redirect(returnURL)
End If
Else
ModelState.AddModelError("LoginFailure", "The username/password combination was invalid")
End If
End If
Return Nothing
Catch ex As Exception
ModelState.AddModelError("LoginFailure", ex.Message)
Return Nothing
End Try
End Function
I've tried eliminating the forms cookie from the equation by not sending it from the windows auth app and just hardcoding the claims and token creation after being redirected back to the main app. The HttpContext.Current.User object stays set as a valid claimsPrincipal each time Application_AuthenticateRequest is hit. I've implemented a Custom AuthorizeAttribute and the user is always authenticated and authorized. The funny thing is if i hit the submit button on the form again immediately after the parameters passed through as null, it works. I've scoured online for a similar problem - nothing - i'm hoping someone on here has an idea.
My answer might be completely unrelated but I'm justtrying to help. We had something similar. When we switched from a web api in MVC4 to MVC5. We had an AuthorizationAttribute that checked the token. We read it, set the principal but when we hit the controller action, the principal was gone (in mVC5 - in the previous version this worked). The short story was that it had to do with the async nature of MVC5. We changed our way to implement another attribute that implemented IAuthenticationFilter (which is where mVC5 expects the principal to be set).
What this has to do with you ? I think you also have a problem with async and that you need to figure out in which event MVC5 expects you to set the principal (Application_AuthenticateRequest might not be the place). Anything else might be set on the wrong thread and be gone when you arrive in the controller.

Issues with storing the custom Principal in Session for ASP.NET MVC

I am running into an issue with ASP.NET MVC where it is forcing the user to log back in after about 20 mins of inactivity.
I am using Forms Authentication and have increased the time-out in the config file as:
<authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="9999999" />
</authentication>
I am also setting the session time-out in the config file as:
<sessionState timeout="120"></sessionState>
I am basing this off of Rockford Lhotka's CSLA ASP.NET MVC example and have the following in my global.asax:
protected void Application_AcquireRequestState(object sender, EventArgs e)
{
if (HttpContext.Current.Handler is IRequiresSessionState)
{
if (Csla.ApplicationContext.AuthenticationType == "Windows")
return;
System.Security.Principal.IPrincipal principal;
try
{
principal = (System.Security.Principal.IPrincipal)
HttpContext.Current.Session[MyMembershipProvider.SESSION_KEY];
}
catch
{
principal = null;
}
if (principal == null)
{
if (this.User.Identity.IsAuthenticated && this.User.Identity is FormsIdentity)
{
// no principal in session, but ASP.NET token
// still valid - so sign out ASP.NET
FormsAuthentication.SignOut();
this.Response.Redirect(this.Request.Url.PathAndQuery);
}
// didn't get a principal from Session, so
// set it to an unauthenticted PTPrincipal
BusinessPrincipal.Logout();
}
else
{
// use the principal from Session
Csla.ApplicationContext.User = principal;
}
}
}
From what I can tell it should ONLY time-out after 120 minutes of inactivity ... but for some reason it always seems to time-out after 20 minutes of inactivity. I have know idea why this is happening, any ideas?
I am toying with the idea of just dumping Forms Authentication and handling it myself via Session, but I'm afraid I would lose functionality like [Authorize] attributes and so on. Trying not to go down this path.
Is it possible to store my custom principal object as a cookie? I just don't want to have to authenticate/authorize a user for every single page or action.
I'm losing hair ... rapidly! =)
Mixing concerns of FormsAuthentication with SessionState is just a bad idea on many levels, as you are noticing from the answers you are getting.
If the information describing your custom principal is small, I would suggest storing it in the UserData member of the forms ticket. That is what it is there for.
Then your custom data, which is only valid with a valid ticket, is stored with the ticket.
Many problems solved and mucho code obviated.
Here is a helper class that can help you with your ticket.
CAVEAT: In practice the max http cookie size is just shy of the official 4k limit and Encryption cuts that in half approximately.
If you can ensure that your ticket, including principal data will fit into <2k you should be good to go. Creating a custom serialization for your principal can help, e.g. name=value pairs works great if your data will cooperate.
Good luck.
Handling it via Session may not be enough. Because it could be IIS recycling your application, therefor causing all the sessions to be abandoned.
See
[recycling] [iis]
[recycle] [iis]
Hopefully you got this solved by now, but in case somebody else comes along with the same issues, I've been responsible for debugging some code written using the same template, and here are a few thoughts:
1) The forms ticket has a timeout encoded into its value. Most of the example code out there hard-codes this timeout instead of pulling from the forms auth configuration, so if you're just looking at your web.config everything can look fine but your custom security code is ignoring the web.config value. Look through your code for "new FormsAuthenticationTicket" and see what you are doing for the expiration time.
2) The forms cookie has a timeout set in its cookie value. Some of the example code out there hard-codes this timeout. Look and see if you are setting cookie.Expires on your security cookie. (Custom auth tends to hand-build more code here than you would expect because the FormsAuthentication methods don't expose the make-a-cookie-with-userdata method, and you generally want to use userdata to store a bit of info like roles in)
3) Some clients will not set a cookie on response redirect. And sometimes even if they do, you'll get back a cookie other than the one you set. For example, if you have changed the app path or domain at any point, it's possible for the user to have two valid cookies, and you're only clearing one when you try to log them back in here. Now, this code basically reads "The user has some session info, and was logged in, but their session didn't contain the principal I expected it to, so I redirect them to login again." Well, if they don't listen to your auth cookie, or have an auth cookie you don't expect (maybe you changed your domain or path values at some point and they have a /oldpath cookie still set), this can infinite loop. I recommend nuking the session server-side as soon as you find out that it doesn't have the data you want: Session.Clear() - this leaves you less likely to end up in this situation after a redirect. (From a recover-server-side-without-trusting-the-client-to-behave perspective, it's actually slightly safer to go ahead and reconstruct the principal object and put it into the session, but I can see how this would be less secure.)
It's also safer to just do a Server.Transfer to the login page rather than relying on a cookie-changing redirect to work right. If you do end up in a redirect loop, server.transfer is guaranteed to end it.
Are you using the membership provider for authorization also? If so you may want to look at the userIsOnlineTimeWindow attribute. The default for this is also 20 minutes.

Implementing autologin for ASP.NET MVC

I've been trying to get membership working in ASP.NET MVC. There are some pages that require authentication. Others that can have guests and authorised members. Much like StackOverflow. They can do things anonymously or as a registered user.
We also have a custom database schema of handling members (not the default ASP.NET Membership way). So I'd be looking at writing my own Membership/Principal code. So far its working well, BUT I'm having trouble with sessions expiring and being able to implement the 'Remember me' functionality.
I use FormsAuthentication.SetAuthCookie(username, rememberMe) to set the forms cookie, but say the user leaves the machine for 20mins or IIS recycles my session dies and I get a very inconsistent user state.
Where do I catch the 'Remember me' cookie and how do I handle the user logging in again? Essentially do I store the username and password and then look for the cookie in Application_BeginRequest or something?
If I read you right, it seems like you are expecting ASP.NET to detect the persistent auth cookie and re-establish that user's last session state. It doesn't work that way: the authentication cookie and asp.net session are two independent things, so indeed you will have instances where a user goes back to the site and passes authentication via a persistent AuthCookie, but has brand new (empty) session data.
Assuming the AuthCookie is being set properly, you can detect that the user is authenticated with User.Identity.IsAuthenticated which is not affected by IIS recycles or session expiring. The username is exposed in User.Identity.Name.
If you need to re-initialize some Session data when a user returns to the site you'll have to do that manually. If this is really what you're asking, then it's hard to answer without knowing more about your app, but consider the Session_Start and Session_End events in the global.asax. Alternatively you can just null-check a session object and re-populate whenever it's empty (after session does expire), eg:
//get some user info from session or db if session is null
string preferredName;
if (User.Identity.IsAuthenticated)
{
object o = Session["preferredName"];
if (o == null)
{
preferredName = repository.GetUser(User.Identity.Name).PreferredName;
Session["preferredName"] = preferredName; //save to session so this isn't necessary next time
}
else
{
preferredName = (string)o;
}
}
else
{
preferredName = "anon";
}

ASP.NET MVC Authorization and hyperlinks

I am using successfully custom authorization in ASP.NET MVC. It simply involves a comparison between User.Identity and the owner of the object in context.
It works like a charm when used in simple conditions. It becomes more complicated when I try to call 2 actions in one web request.
Lets say I want to display an image which would be generated on-the-fly by my application. This image is generated by a controller, thus, it can be referenced by an URL even if it doesn't exist physically. I have decided that the user must be signed in and be the owner to view it, so I apply my authorization mechanizm to it.
Example: <img src="http://myapplication.com/images/generate/3" />
When I include such an image in a page via its action hyperlink, I expect that the authenticated user will still be in context on server side when the image is generating. This is not the case in my tests. The image never displays because my authorization check doesn't work. In the image controller, User.Identity is empty as if the user has not signed it.
In the meantime, the same user is still signed in to the website and can continue to browse with his identity in context... without those images working properly.
I wonder how to make this process work securely...
Thank you very much!
Marc Lacoursiere
RooSoft Computing inc.
Just wondering if you've checked if
Thread.CurrentPrincipal
is also empty in the controller? It should contain the same value.
Another suggestion would be to store the User.Identity value in a session?
You need to set up your identity in global.asax on every request. I'm using a custom Principal and Identity for this.
private void Application_AuthenticateRequest(object sender, EventArgs e)
{
if (!Request.IsAuthenticated)
{
SetIdentity(new MyIdentity
{ Type = UserType.Inactive, Id = int.MinValue });
}
else
{
HttpCookie authCookie = Request.Cookies[
FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
FormsAuthenticationTicket authTicket =
FormsAuthentication.Decrypt(authCookie.Value);
var identity = Repository.GetIdentity
(authTicket.Name, new HttpRequestWrapper(Request));
SetIdentity(identity);
}
}
}
private void SetIdentity(MyIdentity identity)
{
Context.User = new MyPrincipal { Identity = identity };
Thread.CurrentPrincipal = Context.User;
}
This works, but I don't guarantee it to be secure. You should review this article on FormsAuthentication vulnerabilities prior to going live with this code. You have to understand that this code assumes the cookie is valid and hasn't been hijacked. There are some additional security measures that can be taken to reduce these vulnerabilities which this code doesn't show.
This may be when the site link in browser is http:\www.mysite.com (or http:\subdomain.mysite.com ) and you are using http:\mysite.com\image\5 in your application. Form authentication uses cookies. And these cookies may belong to domains and subdomains.
To find out what is going on I suggest to use FireFox with FireBug installed. Enable Net and Console tab for your site and make a complete refresh of the page. After you'll see requests in one of these tabs (Net tab exactly). At the left of the request you can see 'a plus' button, after you click it you'll see Headers and Response tabs (more detailed description of firebug). Have a look at Headers tab and try to find something like FORMAUTH (or what you've set in config as a forms cookie name). If you not see it - the problem is in domains.

Resources