ExternalSignInAsync - when does it return SignInStatus.RequiresVerification? - asp.net-mvc

I am using the standard MVC template from VS 2013 and using logins with External Ids, ie Google, Facebook, etc.
In AccountController.ExternalLoginCallback, it calls SignInManager.ExternalSignInAsync and the result can be one of SignInStatus.Success, .LockedOut, .RequiresVerification, and .Failure.
I have been able to make it return LockedOut by putting a time later than now in column LockoutEndDateUtc of dbo.AspNetUsers.
When or how does it return RequiresVerification?
I want to use this identity framework but with a bit of customization.

If you are using Two-Factor Authentication, then login will return SignInStatus.RequiresVerification for the user to enter the verification code.
See Two-factor authentication using SMS and email with ASP.NET Identity

ExternalSignInAsync return RequiresVerification if :
dbo.ASpnetUsers has for user set to true TwoFactorEnabled and EmailConfirmed and user email should be confirmed, email not be empty or null.

Related

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.

What exactly does Owin rememberBrowser do?

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.

ASP.NET MVC4 WebMatrix WebSecurity Forms authentication: Changing a username

I'm using WebMatrix WebSecurity for my application's Forms Authentication.
The user needs to be able to change his username, without being logged out.
I supposed calling WebSecurity.Logout(), followed by WebSecurity.Login() would do the trick, but Login() requires a password. Of course, I cannot provide this password as it is hashed in the DB.
How can I make this requirement work?
EDIT:
Below are a few suggestions on how to fix the issue of changing the username.
However, my actual problem was that the cookie still holds the old username. I found the following instructions on how to handle that:
http://omaralzabir.com/how_to_change_user_name_in_asp_net_2_0_membership_provider/
So this is how I able to change the user name. This all happens inside a post action method for updating all kinds of user data.
First I check to see if the user name already exists. "info" is a model object coming from the view.
if (WebSecurity.CurrentUserName != info.UserName &&
this.userRepository.Find(info.UserName) != null)
{
ModelState.AddModelError("", "This email is already taken.");
return View();
}
Then I update the database.
UserProfile user = this.userRepository.Find(info.UserId);
user.UserName = info.UserName;
this.userRepository.SaveUser(user);
Then here is the magic part. You have to reset the authorization cookie.
FormsAuthentication.SetAuthCookie(user.UserName, false);
I hope that helps someone out there.
Since WebMatrix WebSecurity is done via UserId, not Username, just change the data in the table you are storing your userdata in, and then redirect them to a new page. You don't need to log them out and back in, I believe the new username will be picked up immediately.
You can do the following steps
Get the UserDetails by UserId or UserName
Change the UserName
Now update the user data by calling the SimpleMembershipProvider.UpdateUser.
More details here

How does one prevent duplicate email addresses while users modify their email addresses after registration using the default MVC Membership Provider?

I am using the default ASP.NET MVC Membership Provider and I would like to allow user's to modify their email after they have created their account. I do not want users to be able to use a duplicate email.
How do I allow a user to modify their email and check that the email is not in use elsewhere in the database? I am not sure of the best way to do this type of check when using the default Membership Provider.
note - I know the Membership Provider itself performs this check when a user attempts to register their email address, I do not know how to perform this check at a later time (due to noobness).
note 2 - I only know of this method of accessing the user's email, is this the proper way to be accessing it?
MembershipUser useremail = Membership.GetUser(User.Identity.Name);
ViewBag.Email = useremail.Email;
You can search for an existing username by that email:
String userName = MembershipProvider.GetUserNameByEmail(email)
If no match is found, userName will be null. See here for more info on this.
Also, if your MembershipProvider has RequiresUniqueEmail = true then this check should already be performed for you - as per this page.

ASP.NET MVC custom membership for beginners

I am creating my own website and blog and I want for first time just me in database (my name and password) and maybe later some registration for others but first log in just for me and administration with authorization. I don´t want to use Membership from MS. I want try to create my own from start so I am looking for guide for beginners but I found big guides with roles, rights. I want just small example with check username, password in database with log on data.
Thanks for help
Libor
Even if you don't want to use the membership and role provider data store you can still utilize the authentication. Trust me, it's a lot easier than building your own. Here's how it works:
We'll say you already have your user storage setup for retrieving the username and their password. For the sake of simplicity I'm going to pretend you have a static class called DataLayer that contains your data retrieval methods for pulling info from the database (or whatever storage you use).
First you need a way to let the user log in. So set up a page with username and password fields. Then in the action method that the page posts to set up a quick if statement:
if (DataLayer.UserExists(userModel.Username))
{
User userFromDB = DataLayer.GetUser(userModel.Username);
if (userFromDB.Password == userModel.Password)
{
FormsAuthentication.SetAuthCookie(userFromDB.Username, checkBoxRememberMe.Checked);
//Use userFromDB as the username to authenticate because it will
//preserve capitalization of their username the way they entered it
//into the database; that way, if they registered as "Bob" but they
//type in "bob" in the login field, they will still be authenticated
//as "Bob" so their comments on your blogs will show their name
//the way they intended it to.
return "Successfully logged in!";
}
}
return "Invalid username or password.";
Now that they are authenticated you can just use Page.User.Identity.IsAuthenticated in your code to find out if they are logged in. LIke this:
if (User.Identity.IsAuthenticated)
{
DataLayer.PostBlogComment(User.Identity.Name, commentBody);
//Then in your controller that renders blog comments you would obviously
//have some logic to get the user from storage by the username, then pull
//their avatar and any other useful information to display along side the
//blog comment. This is just an example.
}
In addition, you can lock out entire action methods or even whole controllers to users that are authenticated through the forms authentication provider. All you have to do is add tags like these to your action methods/controllers:
[Authorize]
public ActionResult SomeActionMethod()
{
return View();
}
The [Authorize] attribute will prevent users that are not logged in from accessing that action method and it will redirect them to your login page. You can use this same attribute to filter out roles if you are using the built in roles provider.
[Authorize(Roles="Admin, SalesReps")]
public ActionResult SomeActionMethod()
{
return View();
}
These attributes can also be added above the controller class to apply it's logic to the entire controller.
EDIT: To log a user out all you need to do is call FormsAuthentication.SignOut();
Hey #Bibo, good for not choosing the Membership providers. I think a UserService or similar which provides methods for creating, authenticating users and some few more methods should be enough. As a suggestion, use password hashing and a password salt for the user´s password. Here is a good link to look at. Also have a look at this answer I gave some time ago.
Good luck!
EDIT: The rememberMe parameter should be named keepMeSignedIn instead.
This article on forms authentication gives you loads of info for creating your own simple security system, especially the bit about FormsAuthenticationTicket.
http://support.microsoft.com/kb/301240

Resources