MVC Owin external login disconnect database - asp.net-mvc

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

Related

ASP Identity New user with externallogin

I'm refactoring an old website to use OWIN and claims with forms and external loigins with ASP Identity 2.
I have a question around the proper way to create a new user with external login.
I'm using the MVC scaffolding code but have a custom UserStore, UserManager and signinmanager and everything is mostly working.
The account control has in the ExternalLoginCallback method has a case with a comment to redirect to ExternalLoginConformation when a user is not found, but I am unsure where to short circuit the login logic so it wont throw an exception.
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 });
}
The SignInManager.ExternalSignInAsync method flows through the userstore and usermanager and signinmanager in the following mannor. Where and how is the best way to short the logic to get the result as a failure?
AccountController : ExternalLogin(Provider, returnURL)
My Owin Middleware
AccountController : ExternalLoginCallBack
AuthenticationManager returns loginInfo with all the details
UserManager : Task FindAsync(UserLoginInfo)
Calls UserStore : Task FindAsync(UserLoginInfo)
UserLoginInfo has Provider and Key and this is where i find there is NO user in the system. No matter what Task i return it wont stop the flow.
...
UserStore : Lockout and other misc stuff - Needs a User object even if empty
...
SignInManager : Task SignInAsyn(User, Persistent, Remeber)- User object is empty
SignInManager : Task CreateUserIdentityAsync(User)- User object is empty
User : Task GenerateUserIdentityAsync(UserManager manager)
UserManager : CreateIdentityAsync(User, Auth type) user is empty and auth type = "external cookie" This throws a NULL Exception.
Solution Found. I can return a null task from the UserManager FindAsync method and it will result in a failure result.
return Task.FromResult<MyUser>(null);
Solution Found. I can return a null task from the UserManager FindAsync method and it will result in a failure result.
return Task.FromResult<MyUser>(null);

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

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)

Linking external logins to existing user

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.

ASP.NET Web API 2: Login with external provider via native mobile (iOS) app

I have done much searching and have not been able to find an ideal solution for this issue. I know that there is an alleged solution (WebApi ASP.NET Identity Facebook login) however, some elements of the solution are a (in my mind) seriously hacky (e.g. registering the user with a regular account and then adding the external login, rather than registering them with the external login).
I would like to be able to register and authenticate against an ASP.NET Web API 2 application, after already having used the Facebook SDK login on a iOS mobile app, i.e. I have already authenticated against Facebook using their SDK, and now want to seamlessly register/authenticate with the ASP.NET Web API. I do not want to use the process where I have to use the web calls (/api/Account/ExternalLogin) as this, well, is not a great user experience on a native mobile app.
I have tried learning about OWIN, but the .NET framework is complex and I am struggling in how to solve this issue.
I needed to do this today for my Ionic app. The Web API Account controller has its own opinion on how to do things and the best way to understand it is reading this pretty amazing 3 part blog post by Dominick Baier. https://leastprivilege.com/2013/11/26/dissecting-the-web-api-individual-accounts-templatepart-3-external-accounts/.
The way I worked around it was to forget the out-of-the-box flow, but instead use the accessToken from the native Facebook login and then call into the following server code to 1) call the Facebook API to validate the access token, 2) from that Facebook call, get the email and id, 3) either get the user or create it (and login) which is already code that's in the Account controller in other places, 4) Create the local authority JWT for subsequent Web API calls.
public class ProviderAndAccessToken {
public string Token { get; set; }
public string Provider { get; set; }
}
[AllowAnonymous]
[HttpPost]
[Route("JwtFromProviderAccessToken")]
public async Task<IHttpActionResult> JwtFromProviderAccessToken(ProviderAndAccessToken model) {
string id = null;
string userName = null;
if (model.Provider == "Facebook") {
var fbclient = new Facebook.FacebookClient(model.Token);
dynamic fb = fbclient.Get("/me?locale=en_US&fields=name,email");
id = fb.id;
userName = fb.email;
}
//TODO: Google, LinkedIn
ApplicationUser user = await UserManager.FindAsync(new UserLoginInfo(model.Provider, id));
bool hasRegistered = user != null;
string accessToken = null;
var identity = new ClaimsIdentity(OAuthDefaults.AuthenticationType);
var props = new AuthenticationProperties() {
IssuedUtc = DateTime.UtcNow,
ExpiresUtc = DateTime.UtcNow.Add(Startup.OAuthOptions.AccessTokenExpireTimeSpan),
};
if (hasRegistered) {
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(UserManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookieIdentity = await user.GenerateUserIdentityAsync(UserManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = ApplicationOAuthProvider.CreateProperties(user.UserName);
Authentication.SignIn(properties, oAuthIdentity, cookieIdentity);
identity.AddClaim(new Claim(ClaimTypes.Name, user.UserName));
identity.AddClaim(new Claim("role", "user"));
}
else {
user = new ApplicationUser() { UserName = userName, Email = userName };
IdentityResult result = await UserManager.CreateAsync(user);
if (!result.Succeeded) {
return GetErrorResult(result);
}
result = await UserManager.AddLoginAsync(user.Id, new UserLoginInfo(model.Provider, id));
if (!result.Succeeded) {
return GetErrorResult(result);
}
identity.AddClaim(new Claim(ClaimTypes.Name, userName));
}
identity.AddClaim(new Claim("role", "user"));
var ticket = new AuthenticationTicket(identity, props);
accessToken = Startup.OAuthOptions.AccessTokenFormat.Protect(ticket);
return Ok(accessToken);
}
The code I'm using in Ionic basically does this to get the access token from Facebook, then call the Web API to get a local authority JWT to use as the Bearer token.
Facebook.login(['public_profile', 'email']).then((result) => {
return this.http.post("<URL>/api/Account/JwtFromProviderAccessToken", { provider: "Facebook", token: result.authResponse.accessToken })
.map((res: Response) => res.json())
.catch(this.handleError)
.subscribe((res: Response) => {
// use the result as the Bearer token
});
})...
Seems pretty safe, but understand that I'm not security expert so this code comes without warranty and please let me know if you see anything glaring and I'll update the code.

Resources