Linking external logins to existing user - asp.net-mvc

I'm quite new to Identity and trying to learn by watching videos in https://channel9.msdn.com/Series/Customizing-ASPNET-Authentication-with-Identity
In the default ASP.Net MVC template, you can link multiple external logins (google, facebook) to your account (through /Manage) if you are already logged in.
But what if the user first logged in to our website using their google account and log out from it and on another day tried to login using their facebook account. Assuming both of their facebook and google accounts uses the same email address, the user will not be able to login to the website because the default template doesn't allow that as UserManager.CreateAsync is going to fail. I know they can change their email and login, but that will create two different accounts for the same user.
var info = await AuthenticationManager.GetExternalLoginInfoAsync();
if (info == null)
{
return View("ExternalLoginFailure");
}
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await UserManager.CreateAsync(user);
if (result.Succeeded)
{
result = await UserManager.AddLoginAsync(user.Id, info.Login);
if (result.Succeeded)
{
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
return RedirectToLocal(returnUrl);
}
}
I altered the code in ExternalLoginConfirmation so that it checks if the user exist and ads the new external provider to AspNetUserLogins. Can anyone please tell me if this is the right way to do this? or if there is a better way of doing it.
if (ModelState.IsValid)
{
// Get the information about the user from the external login provider
var info = await AuthenticationManager.GetExternalLoginInfoAsync();
if (info == null)
{
return View("ExternalLoginFailure");
}
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await UserManager.CreateAsync(user);
if (result.Succeeded)
{
result = await UserManager.AddLoginAsync(user.Id, info.Login);
if (result.Succeeded)
{
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
return RedirectToLocal(returnUrl);
}
}
//new code begins
else if (UserManager.FindByEmail(model.Email) != null)
{
var usr = await UserManager.FindByEmailAsync(model.Email);
result = await UserManager.AddLoginAsync(usr.Id, info.Login);
if (result.Succeeded)
{
await SignInManager.SignInAsync(usr, isPersistent: false, rememberBrowser: false);
return RedirectToLocal(returnUrl);
}
}
//new code ends
AddErrors(result);
}

I'm also fairly new to Identity and have come across the same problem, though my solution is quite different. I don't use Entity Framework and to achieve this I've had to basically rewrite the entire Identity engine with custom classes. I have my own Sql Server tables which are different to those created by EF. EF stores identity in 5 tables: users, roles, userroles, userclaims and userlogins. I only use the first three. In my environment AddLoginAsync is not required because the table doesn't exist. I store all local/external logins and registered users in the user table. Claims are stored as userroles when required.
The way I got around duplicate UserNames and Emails (logging in using different providers with the same registered email addresses) was to remove the validation check for existing usernames and emails before creating a user, using a custom UserValidator. The table allows for duplicates. When logging in I do a custom check for uniqueness based on username/provider (external) or email/passwordhash (local). It seems to be working.

Related

Invalidate ClaimsPrincipal after it has been modified

I am using ASP.NET MVC, Identity2.
I have added "FirstName" Custom ClaimPrincipal:
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser, long> manager)
{
var userIdentity = await manager.CreateIdentityAsync(
this,
DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
userIdentity.AddClaim(new Claim("FirstName", FirstName));
return userIdentity;
}
If I update the value of "FirstName", I need to logout and log back in, for the "FirstName" Claim to be updated. Is it possible to invalidate "FirstName" Claim, so it's value is forced to be refreshed?
I have seen this question, which shows how to update the value of Claims, I was wondering if there is easier way to just invalidate them.
When looking at MS built-in template, I noticed that they alway makes a call to SignInManager.SignInAsync, after changing user credentials (e.g. password, 2 Factor Authentication, etc).
I also noticed that the Claims are updated once the user logs out and logs back in... so after changing "FirstName" which is stored in a Claim, I called SignInManager.SignInAsync to re-signin the User... this way, the Claims are updated:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> UpdateFirstName(string firstName)
{
var user = await UserManager.FindByIdAsync(User.Identity.GetUserId<long>());
user.FirstName = firstName;
// update FirstName which is stored in a Claim
var result = await UserManager.UpdateAsync(user);
if (result.Succeeded)
{
// re-signin the user, to refresh the Claims
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
// you need to redirect after calling SignInAsync, so claims are refreshed
return RedirectToAction("Index");
}
// add some error message...
return View();
}
Note: As shown in the question, I am storing the Claims in Cookie.

MVC Owin external login disconnect database

I have a basic MVC 5 site setup for user accounts and external logins.
For external logins I am using on-premises ADFS OpenID Connect for employees so it should be similar to Azure AD.
So in this case the MVC Identity 2.0 verbiage of External Login is actually for Internal Users (Employees). Employees will use ADFS and the public will have user accounts in the aspnet database.
The normal workflow in the MVC template, adds external users to the aspnet database. The workflow adds them or checks if the users are in the appropriate tables in this database.
I want to disconnect this database for employees since they are already authenticated by ADFS because I can authorize them using our AD and claims to there is no reason to add them to the database.
Here is the standard code in the AccountController.
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
// Sign in the user with this external login provider if the user already has a login
//var result = await SignInManager.ExternalSignInAsync(loginInfo, isPersistent: false);
var result = await SignInManager.ExternalSignInAsync(loginInfo, isPersistent: false);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = false });
case SignInStatus.Failure:
default:
// If the user does not have an account, then prompt the user to create an account
ViewBag.ReturnUrl = returnUrl;
ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = loginInfo.Email });
}
}
Here the loginInfo shows the user is authenticated.
The result variable is false since the user is not in the aspnet database and so would send the user to the ExternalLoginConfirmation view.
If I modify the switch statement to send the user to the returnUrl instead, the Authenticate attribute on that controller action sees the user as not authenticated and starts the process over again since the user is not authenticated according to the built-in workflow.
How do I intercept this workflow and satisfy the Authorize attribute? I have checked these properties and they return false.
var authenticated = HttpContext.User.Identity.IsAuthenticated;
var authenticated2 = HttpContext.Request.IsAuthenticated;
var authenticated3 = HttpContext.GetOwinContext().Authentication;
I was able to disconnect the aspnet database from the workflow by removing the code which checks the database tables.
The new code signs out of the external cookie, generates a new application cookie and copies the claims from the external cookie to the application cookie.
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
//We signed in with an external provider which creates and external cookie but this is not sufficient for application
//authorization so we need to create an application cookie and copy the existing claims and add new claims as needed
var externalClaims = loginInfo.ExternalIdentity.Claims;//get the current external claims for the user
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);//sign out of external authentication
var claims = externalClaims.ToList();//create a new list of the external claims
//add the identityprovider claim since this is needed for the AntiForgeryToken
claims.Add(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", loginInfo.DefaultUserName));
var identity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie, ClaimTypes.Name, ClaimTypes.Role);//create new identity
AuthenticationManager.SignIn(identity);//sign in with the new local identity containing an application cookie
ViewBag.ReturnUrl = returnUrl;
ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
return RedirectToLocal(returnUrl);
}

ASP.NET MVC Authentication using External Provider - Google/Facebook

I am using Microsoft/Google/Facebook Authentication in my asp.net MVC application, which authenticates user & redirect users to my site. This works fine.
Issue : Anyone having a Microsoft/Google/Facebook account can able to sign in to my application. I should only allow users those who are registered/mapped in our database ie if a person purchased a licences only he should able to login using external provider.
Example :
1) User1 has a Microsoft/Google account & user1 is a valid user in our database. So we can allow him to see the content from our site.
2) user2 has a microsoft/Google account, but he isn't valid user in our db. He shouldn't able to gain access to our site.
How can I achieve this in ASP.NET MVC. I am using client id & client secret key from external providers.
sample code from startup class
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
{
ClientId = "",
ClientSecret = ""
});
In AccountController.cs (default code if you haven't changed it)
[AllowAnonymous]
public ActionResult ExternalLoginCallback(string returnUrl)
{
AuthenticationResult result = OAuthWebSecurity.VerifyAuthentication(Url.Action("ExternalLoginCallback", new { ReturnUrl = returnUrl }));
if (!result.IsSuccessful)
{
return RedirectToAction("ExternalLoginFailure");
}
if(EmailNotRegistered(result.ExtraData["userid"]))
{
FormsAuthentication.SignOut();
return RedirectToAction("ExternalLoginFailure");
}
var bresult = OAuthWebSecurity.Login(result.Provider, result.ProviderUserId, createPersistentCookie: false);
if (bresult)
{
return RedirectToLocal(returnUrl);
}
// etc...
}
You will need to write the function bool EmailNotRegistered(string email) and do the logic where it checks the local database. There might be something already available in the Membership API to check but I don't know right now.
By the way, each provider is different so the ExtraData field might be "email" or something else - use the debugger to find out!
Here is my AccountController
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
// Sign in the user with this external login provider if the user already has a login
var result = await SignInManager.ExternalSignInAsync(loginInfo, isPersistent: false);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = false });
case SignInStatus.Failure:
default:
// If the user does not have an account, then prompt the user to create an account
ViewBag.ReturnUrl = returnUrl;
ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = loginInfo.Email });
}
}

ASP.NET MVC Login by email instead of username

I try to create new project with MVC 5 and OWIN by "Individial User Accounts". The problem is when I register a new account, the system requires me input an email then that email will be populated to both Email and Username in database.
When I try to login, the system asks me to input the email but it compares that email with column Username. So the values of Email and Username are the same. The login code below is in action Login (Account Controller)
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
Is there any ways to compare the inputted email with the Email column instead of Username?
To answer:
Is there any ways to compare the inputted email with the Email column instead of Username?
This is probably not the best answer but the SignInManager should have access to UserManager which has a Users property.
So you could lookup the user based on the email and then using the result call the signin method.
Something like this (untested):
var user = SignInManager.UserManage.Users.Where(u => u.email == model.Email).FirstOrDefault();
var result = await SignInManager.PasswordSignInAsync(user.UserName, model.Password, model.RememberMe, shouldLockout: false);
You would want to check that the user was populated otherwise return invalid login details error. Same as if PasswordSignInAsync failed.
Here is what to do as #ChrisMoutray suggested, find the username for the email provided and log in. I put it here.
Instantiate usermanager and signinmanager
private readonly SignInManager<ApplicationUser> signInManager;
private readonly UserManager<ApplicationUser> userManager;
public AccountController(SignInManager<ApplicationUser> signInManager, UserManager<ApplicationUser> userManager)
{
this.signInManager = signInManager;
this.userManager = userManager;
}
and then in the Login (post) task:
var user = await userManager.FindByEmailAsync(model.Email);
var result = await signInManager.PasswordSignInAsync(user.UserName, model.Password, model.RememberMe, lockoutOnFailure: false);
PS. Found myself in this old post so just adding a working solution.
The reason is because when you register. you get username = email.
public async Task<ActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await UserManager.CreateAsync(user, model.Password);

prevent users without confirmed email from logging in ASP.Net MVC with Identity 2

In microsoft Identity 2 there is ability to users can confirm there email addresses I downloaded Identity 2 sample project from here in this project there isn't any difference between users confirmed their emails and who doesn't I want to people how don't confirmed their emails can't login this is what I tried :
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: true);
switch (result)
{
case SignInStatus.Success:
{
var user = await UserManager.FindByNameAsync(model.Email);
if (user != null)
{
if (!await UserManager.IsEmailConfirmedAsync(user.Id))
{
//first I tried this.
//return LogOff();
HttpContext.Server.TransferRequest("~/Account/LogOff");
return RedirectToAction("Login");
}
}
return RedirectToLocal(returnUrl);
}
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
I tried to force user to Logoff by calling LogOff() action method but It didn't work and user remain authenticated .then I tried to use Server.TransferRequest() but I don't know why it did the job but it redirects users to login page with returnUrl="Account/Logoff"
so after they confirmed their email and tried to login they get logoff I get really confused!!
this is my LogOff() action method:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
AuthenticationManager.SignOut();
return RedirectToAction("About", "Home");
}
I have googled it for days without any luck !!!!
Maybe its a little late but I hope it may help others.
Add this
var userid = UserManager.FindByEmail(model.Email).Id;
if (!UserManager.IsEmailConfirmed(userid))
{
return View("EmailNotConfirmed");
}
before
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
The first block of code just checks if the email in the model exists in the database and gets it's id to check if it is not confirmed and if so returns a view to the user wich says so and if it is confirmed just lets the user sign in.
And delete your changes to the result switch like this
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
Instead of moving to another page, why not finish this one and redirect to the right action / view:
if (!await UserManager.IsEmailConfirmedAsync(user.Id))
{
return RedirectToAction("ConfirmEmailAddress", new { ReturnUrl = returnUrl });
}
You do need an action (and possibly a view) with the name ConfirmEmailAddress though.
There is a solution, which may not be the best approach, but it works. First let me try to clarify why your approach did not work.
In one of the comments it is mentioned, the AuthenticationManager uses cookies. In order to update a cookie you need to send it to the client, using another page. That is why TransferRequest is not going to work.
How to handle the emailverification? The strategy I used:
1) On SignInStatus.Success this means that the user is logged in.
2) When email is not confirmed: send an email to the used e-mailaddress. This is safe since the user already signed in. We are just blocking further access until the e-mail is verified. For each time a user tries to login without having validated the email, a new email (with the same link) is sent. This could be limited by keeping track of the number of sent emails.
3) We cannot use LogOff: this is HttpPost and uses a ValidateAntiForgeryToken.
4) Redirect to a page (HttpGet, authorization required) that displays the message that an e-mail has been sent. On entering sign out the user.
5) For other validation errors, redirect to another method to sign out (HttpGet, authorization required). No view needed, redirect to the login page.
In code: update the code in AccountController.Login to:
case SignInStatus.Success:
{
var currentUser = UserManager.FindByNameAsync(model.Email);
if (!await UserManager.IsEmailConfirmedAsync(currentUser.Id))
{
// Send email
var code = await UserManager.GenerateEmailConfirmationTokenAsync(currentUser.Id);
var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = currentUser.Id, code = code}, protocol: Request.Url.Scheme);
await UserManager.SendEmailAsync(currentUser.Id, "Confirm your account", string.Format("Please confirm your account by clicking this link: link", callbackUrl));
// Show message
return RedirectToAction("DisplayEmail");
}
// Some validation
if (true)
{
return RedirectToAction("SilentLogOff");
}
return RedirectToLocal(returnUrl);
}
Add methods to AccountController:
// GET: /Account/SilentLogOff
[HttpGet]
[Authorize]
public ActionResult SilentLogOff()
{
// Sign out and redirect to Login
AuthenticationManager.SignOut();
return RedirectToAction("Login");
}
// GET: /Account/DisplayEmail
[HttpGet]
[Authorize]
public ActionResult DisplayEmail()
{
// Sign out and show DisplayEmail view
AuthenticationManager.SignOut();
return View();
}
DisplayEmail.cshtml
#{
ViewBag.Title = "Verify e-mail";
}
<h2>#ViewBag.Title.</h2>
<p class="text-info">
Please check your email and confirm your email address.
</p>
You'll notice that the user cannot reach other pages until email is verified. And we are able to use the features of the SignInManager.
There is one possible problem (that I can think of) with this approach, the user is logged in for the time that the email is sent and the user is being redirected to the DisplayMessage view. This may not be a real problem, but it shows that we are not preventing the user from logging in, only denying further access after logging in by automatically logging out the user.
=== Update ====
Please note that exceptions have to be handled properly. The user is granted access and then access is revoked in this scenario. But in case an exception occurs before signing out and this exception was not catched, the user remains logged in.
An exception can occur when the mailserver is not available or the credentials are empty or invalid.
===============
I would let the admin create the user without any password. The email with link should go to the user. The user then is directed to SetPassword page to set new password. This way no one can access the user account unless he confirms and sets the password.
Call CreateAsync without the password
var adminresult = await UserManager.CreateAsync(user);
Redirect admin to new custom view saying something like "Email is sent to user"
#{
ViewBag.Title = "New User created and Email is Sent";
}
<h2>#ViewBag.Title.</h2>
<p class="text-info">
The New User has to follow the instructions to complete the user creation process.
</p>
<p class="text-danger">
Please change this code to register an email service in IdentityConfig to send an email.
</p>
The answer by #INFINITY_18 may cause Object reference not set to an instance of an object error if the email does not exist in the data store at all. And why not return the Login view with model error in this case, too?
I would suggest the following:
var userid = UserManager.FindByEmail(model.Email)?.Id;
if (string.IsNullOrEmpty(userid) || !UserManager.IsEmailConfirmed(userid)))
{
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
Require email confirmation
It's a best practice to confirm the email of a new user registration to verify they are not impersonating someone else (that is, they haven't registered with someone else's email). Suppose you had a discussion forum, and you wanted to prevent "yli#example.com" from registering as "nolivetto#contoso.com." Without email confirmation, "nolivetto#contoso.com" could get unwanted email from your app. Suppose the user accidentally registered as "ylo#example.com" and hadn't noticed the misspelling of "yli," they wouldn't be able to use password recovery because the app doesn't have their correct email. Email confirmation provides only limited protection from bots and doesn't provide protection from determined spammers who have many working email aliases they can use to register.
You generally want to prevent new users from posting any data to your web site before they have a confirmed email.
Update ConfigureServices to require a confirmed email:
​
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
services.AddMvc();
services.Configure<AuthMessageSenderOptions>(Configuration);
}

Resources