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

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 });
}
}

Related

Redirect after login in mvc5

In MVC, when writing a project, it writes a login scenario, the login screen, and the registry, and everything.
I created the admin page, but before you go to the page if I have not created a cookie, send me the login page. I want to do it after you log in. I do not know how to redirect to the admin page after logging in.
You have the code itself that writes the viewbag.retutnurl while you are building a project, but I do not know what the controller is.
Now I'm not sure where the value comes from.
If anyone knows exactly and done, please advise.
In the default generated method change the first case, which executes when the login is successful, and add you custom redirection here like this:
switch (result)
{
case SignInStatus.Success:
return RedirectToAction("Index", "Admin", null); // new code
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 });
}
And in your admin class you should put the [Authorize] data annotation
//admin controller
[Authorize]
public ActionResult Index()
{
return View();
}
You can try any of these two ways to deal with it.
public ActionResult Index() {
return RedirectToAction("AdminAction");
//Or you can try this
return RedirectToAction("whateverAction", "whateverController");
}

Authorized sections of .net web app are accessible prior to confirmation email being clicked

I'm using .net 4.5.2 and sendgrid. I've used the link below as a guide but rather than using sendgrid v2 i'm using sendgrid v3.
https://learn.microsoft.com/en-us/aspnet/identity/overview/features-api/account-confirmation-and-password-recovery-with-aspnet-identity
The email confirmation works with a link sent to the registered users email address. When that link is clicked the "Email Confirmation" field in AspNetUsers goes from false to true.
But when the user first submits the registration form - and prior to clicking the confirm email - they become logged into the system. Somehow _LoginPartial is being invoked because the users email address and logoff end up being at the top of the navbar.
So after thinking about it a bit the login action in ActionController is obviously being called just after registration but before email confirmation is clicked. That's not in the Microsoft doc i don't think.
But any advice to fix that would be great. I could check the AspNetUser table for EmailConfirmation == false. But is there a right place to do that?
I checked out this post Prevent login when EmailConfirmed is false and commented out the default login action code and replaced it with this below but it didn't seem to make a difference.
if (ModelState.IsValid)
{
var user = await UserManager.FindByNameAsync(model.Email);
if (user == null)
{
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
//Add this to check if the email was confirmed.
if (!await UserManager.IsEmailConfirmedAsync(user.Id))
{
ModelState.AddModelError("", "You need to confirm your email.");
return View(model);
}
if (await UserManager.IsLockedOutAsync(user.Id))
{
return View("Lockout");
}
if (await UserManager.CheckPasswordAsync(user, model.Password))
{
// Uncomment to enable lockout when password login fails
//await UserManager.ResetAccessFailedCountAsync(user.Id);
return await LoginCommon(user, model.RememberMe, returnUrl);
}
else
{
// Uncomment to enable lockout when password login fails
//await UserManager.AccessFailedAsync(user.Id);
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
}
// If we got this far, something failed, redisplay form
return View(model);
The register action:
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);
if (result.Succeeded)
{
await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);
// For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771
// Send an email with this link
/*These bottom three lines were commented out */
string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please confirm your account by clicking ");
return RedirectToAction("ConfirmRegistration");
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
Login action:
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, change to shouldLockout: true
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: 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 = model.RememberMe });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
}
In your Register action, comment/remove the line:
await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);
That will disable the automatic sign in upon registering. Then, in your Login action, add the following code after the initial ModelState.IsValid check, to check if the user's email has been confirmed:
var user = await UserManager.FindByEmailAsync(model.Email);
if (user != null && !await UserManager.IsEmailConfirmedAsync(user.Id))
{
ModelState.AddModelError("", "Please confirm your email address before signing in.");
return View(model);
}

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);
}

Prevent multiple logins

I am trying to block multiple logins with the same user in my application. My idea is to update the security stamp when user signin and add that as a Claim, then in every single request comparing the stamp from the cookie with the one in the database. This is how I've implemented that:
public virtual async Task<ActionResult> Login([Bind(Include = "Email,Password,RememberMe")] LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
SignInStatus result =
await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, false);
switch (result)
{
case SignInStatus.Success:
var user = UserManager.FindByEmail(model.Email);
var id = user.Id;
UserManager.UpdateSecurityStamp(user.Id);
var securityStamp = UserManager.FindByEmail(model.Email).SecurityStamp;
UserManager.AddClaim(id, new Claim("SecurityStamp", securityStamp));
Then in authentication configuration I've added
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = ctx =>
{
var ret = Task.Run(() =>
{
Claim claim = ctx.Identity.FindFirst("SecurityStamp");
if (claim != null)
{
var userManager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()));
var user = userManager.FindById(ctx.Identity.GetUserId());
// invalidate session, if SecurityStamp has changed
if (user != null && user.SecurityStamp != null && user.SecurityStamp != claim.Value)
{
ctx.RejectIdentity();
}
}
});
return ret;
}
}
});
As it shows I have tried to compare the claim from the cookie with the one in the database and reject the identity if they are not the same.
Now, each time the user signs in the security stamp gets updated but the value is different in user's cookie which I can't find out why? I am suspicious maybe it the new updated security stamp doesn't get stored in user's cookie?
The solution is somewhat more simple than you have started implementing. But the idea is the same: every time user logs in, change their security stamp. And this will invalidate all other login sessions. Thus will teach users not to share their password.
I have just created a new MVC5 application from standard VS2013 template and successfully managed to implement what you want to do.
Login method. You need to change the security stamp BEFORE you create auth cookie, as after the cookie is set, you can't easily update the values:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
// check if username/password pair match.
var loggedinUser = await UserManager.FindAsync(model.Email, model.Password);
if (loggedinUser != null)
{
// change the security stamp only on correct username/password
await UserManager.UpdateSecurityStampAsync(loggedinUser.Id);
}
// do sign-in
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: 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 = model.RememberMe });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
}
This way every login will do an update on the user record with the new security stamp. Updating security stamp is only a matter of await UserManager.UpdateSecurityStampAsync(user.Id); - much simplier than you imagined.
Next step is to check for security stamp on every request. You already found the best hook-in point in Startup.Auth.cs but you again overcomplicated. The framework already does what you need to do, you need to tweak it slightly:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
// other stuff
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(0), // <-- Note the timer is set for zero
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
The time interval is set for zero - means the framework on every request will compare user's security stamp with the database. If stamp in the cookie does not match the stamp in the database, user's auth-cookie is thrown out, asking them to logout.
However, note that this will bear an extra request to your database on every HTTP request from a user. On a large user-base this can be expensive and you can somewhat increase the checking interval to a couple minutes - will give you less requests to your DB, but still will carry your message about not sharing the login details.
Full source in github
More information in a blog-post
In the past I've used IAuthorizationFilter and static logged-in user collection to achieve this:
public static class WebAppData
{
public static ConcurrentDictionary<string, AppUser> Users = new ConcurrentDictionary<string, AppUser>();
}
public class AuthorisationAttribute : FilterAttribute, IAuthorizationFilter {
public void OnAuthorization(AuthorizationContext filterContext){
...
Handle claims authentication
...
AppUser id = WebAppData.Users.Where(u=>u.Key ==userName).Select(u=>u.Value).FirstOrDefault();
if (id == null){
id = new AppUser {...} ;
id.SessionId = filterContext.HttpContext.Session.SessionID;
WebAppData.Users.TryAdd(userName, id);
}
else
{
if (id.SessionId != filterContext.HttpContext.Session.SessionID)
{
FormsAuthentication.SignOut();
...
return appropriate error response depending is it ajax request or not
...
}
}
}
}
On logout:
WebAppData.Users.TryRemove(userName, out user)

ASP.NET MVC Login and Redirect Based On Role

I am using the default Login method generated by ASP.NET MVC and wanted to change it so that it will redirect to a specified View based on the user's role. I have checked that the user is in that role. I made the redirect inside the SignInStatus success block with no success.
I use the User.IsInRole() in other blocks of code and works fine. I think the user is not fully logged in when the if statements are executed. I think this is the case, but I am not sure what work around I can implement.
Here is my code below.
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateCustomAntiForgeryTokenAttribute]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, change to shouldLockout: true
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
switch (result)
{
case SignInStatus.Success:
if (User.IsInRole("Customer"))
{
return RedirectToAction("Customer", "Home");
}
else if (User.IsInRole("Requestor"))
{
return RedirectToAction("Caterer", "Home");
}
else if (User.IsInRole("Admin"))
{
return RedirectToAction("Admin", "Home");
}
else
{
return RedirectToLocal(returnUrl);
}
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
}
Thanks to #stephen.vakil's link I managed to get it to work by changing the block of code inside the SignInStatus.Success case.
case SignInStatus.Success:
var user = await UserManager.FindAsync(model.Email, model.Password);
var roles = await UserManager.GetRolesAsync(user.Id);
if (roles.Contains("Customer"))
{
return RedirectToAction("Customer", "Home");
}
else if (roles.Contains("Requestor"))
{
return RedirectToAction("Caterer", "Home");
}
else if (roles.Contains("Admin"))
{
return RedirectToAction("Admin", "Home");
}
else
{
return RedirectToLocal(returnUrl);
}
......
You are correct. The User object you're referencing here gets set by the "Authentication" step in the ASP.NET pipeline. For more information about this, check out Lifecycle of an ASP.NET 5 Application
PasswordSignInAsync only validates your user and sets up the authentication cookie for future requests. It doesn't affect the User object, which will still represent an unauthenticated state as fed through the pipeline.
A simple way to achieve what you want is to have your Login method redirect to another action (something like RedirectUser) which then performs Role-based routing. This method will have full access to the authenticated User object and the IsInRole method.
Or, you could implement your own User.IsInRole method that queries your DB directly.
[HttpPost]
public async Task<IActionResult> SignIn([FromForm]LoginDto userDto, string returnUrl)
{
if (ModelState.IsValid)
{
//var googlereCaptcha = _googlereCaptchaService.ResponceVerify(userDto.ReCaptchaToken);
//if (!googlereCaptcha.Result.success && googlereCaptcha.Result.score <= 0.5)
//{
// TempData["LoginSuccessMsg"] = "You are not Human.";
// return await Task.Run(() => View("SignIn"));
//}
var signedUser = await userManager.FindByEmailAsync(userDto.Email);
var result = await signInManager.PasswordSignInAsync(signedUser.Email, userDto.Password, userDto.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
{
return LocalRedirect(returnUrl);
}
else
{
var roles = await userManager.GetRolesAsync(signedUser);
if (roles.Contains("Super Admin"))
return RedirectToAction("Dashboard", "User");
if (roles.Contains("Data Entry Operator"))
return RedirectToAction("BusinessList", "Business");
if (roles.Contains("Business Admin"))
return RedirectToAction("MyBusiness", "Business");
}
}
ModelState.AddModelError(string.Empty, "Invalid Login Attempt");
}
return await Task.Run(() => View(userDto));
}
If you Want to user default login of asp.net Identity then you should get role like this after success result and then redirect will fix this issue
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
switch (result)
{
case SignInStatus.Success:
var userId = SignInManager.AuthenticationManager.AuthenticationResponseGrant.Identity.GetUserId();
if (UserManager.IsInRole(userId, "Super Admin"))
{
return RedirectToAction("Index", "DashBoard");
}}

Resources