Prevent multiple logins - asp.net-mvc

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)

Related

How to implement remember me functionality properly? Asp.Net Core

I have been trying for 2 days to know how to build remember me functionality, but there is nothing clear.
First and foremost, I would like to make sure we agreed of the workflow of this properly as follows.
I need here to allow users to open their profile with no need to
signIn again for 1 month, as long as the user doesn't logOut.
I used cookie-based authentication to store some data that I can check every time when user profile opened to make sure that user is authenticated.
-- there is no problem with this step
I use in this step simple code to retrieve data again from the cookie.
-- and here is the problem comes. I can retrieve data from the cookie as long as I'm loggedIn, otherwise, when I stop and re-run the application and redirect to the user profile directly without logIn again I can't read the cookie data although it still exists!!!
Now let's take a look at code
Startup File Cookie Setting
public void ConfigureServices(IServiceCollection services){
.....
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options => {
options.Cookie.Name = "RememberMecookie"; // cookie name
options.LoginPath = "/Account/LogIn"; // view where the cookie will be issued for the first time
options.ExpireTimeSpan = TimeSpan.FromDays(30); // time for the cookei to last in the browser
options.SlidingExpiration = true; // the cookie would be re-issued on any request half way through the ExpireTimeSpan
options.EventsType = typeof(CookieAuthEvent);
});
.....
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
.....
app.UseAuthentication();
app.UseAuthorization();
app.UseCookiePolicy();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
}
.....
public class CookieAuthEvent : CookieAuthenticationEvents
{
public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
context.Request.HttpContext.Items.Add("ExpiresUTC", context.Properties.ExpiresUtc);
}
}
}
Login ViewModel
public class VMLogin
{
public string UserName { get; set; }
public string Password { get; set; }
public bool RememberMe { get; set; }
}
Controller/Login
[HttpPost]
public async Task<IActionResult> LoginAsync(VMLogin CurrentUserLog, string returnUrl)
{
if (!string.IsNullOrEmpty(CurrentUserLog.UserName) && string.IsNullOrEmpty(CurrentUserLog.Password))
{
return RedirectToAction("Login");
}
if (ModelState.IsValid)
{
var SignInStatus = await signInManager.PasswordSignInAsync
(CurrentUserLog.UserName, CurrentUserLog.Password, CurrentUserLog.RememberMe, false);
AppUser _user = await userManager.FindByNameAsync(CurrentUserLog.UserName);
if (SignInStatus.Succeeded)
{
if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl)) // to prevent login from outside link
{
return Redirect(returnUrl);
}
else
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, CurrentUserLog.UserName),
new Claim(ClaimTypes.Email, _user.Email),
new Claim(ClaimTypes.NameIdentifier, _user.Id.ToString())
};
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
var props = new AuthenticationProperties{
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddMonths(1)
};
// to register the cookie to the browser
HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, props).Wait();
return RedirectToAction("UserProfile");
}
}
ModelState.AddModelError(string.Empty, "Invalid Login Attempt");
}
return View(CurrentUserLog);
}
Here is all the problem. I get data from the cookie when I logIn for the first time with the first creation of the cookie as shown in
the code above. However, I can't get the same date from the same
cookie when I stop debugging and run the app again, and redirect to
UserProfile directly without logIn, although the cookie "RememberMecookie" still exists.
Controller/UserProfile
[Authorize]
public async Task<IActionResult> UserProfile()
{
// all lines of code below are working just with the first creation of the cookie with the first login. but if rerun the app again, they all return null if redirect here directly without logIn.
string userId = User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)?.Value;
Claim v = HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier);
AppUser _user = await userManager.GetUserAsync(HttpContext.User);
string cookieValueFromReq = Request.Cookies["RememberMecookie"];
// this is for normal login without remember me functionality
//AppUser user = await userManager.GetUserAsync(User);
return View(/*user*/);
}
Thanks For all guys who spent time checking out my question. I finally found the problem. This code is really great and it can be a good reference for remembering me functionality using cookie-based Authentication. And there is no problem with the code itself.
The problem was with my Startup file
It was like this
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options => {
options.Cookie.Name = "RememberMeBlogAcademy";
options.LoginPath = "/Account/LogIn";
//options.LogoutPath = "/Home/Index";
//options.AccessDeniedPath = "AccessDenied";
options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.SlidingExpiration = true; // the cookie would be re-issued on any request half way through the ExpireTimeSpan
//options.Cookie.Expiration = TimeSpan.FromDays(5);
options.EventsType = typeof(CookieAuthEvent);
});
//services.AddScoped<CookieAuthEvent>();
services.AddControllersWithViews();
The problem was using MVC and AddControllersWithViews together. I didn't know that would make a problem.
However, It should be like this -- using AddControllersWithViews
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options => {
options.Cookie.Name = "RememberMeBlogAcademy";
options.LoginPath = "/Account/LogIn";
//options.LogoutPath = "/Home/Index";
//options.AccessDeniedPath = "AccessDenied";
options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.SlidingExpiration = true; // the cookie would be re-issued on any request half way through the ExpireTimeSpan
//options.Cookie.Expiration = TimeSpan.FromDays(5);
options.EventsType = typeof(CookieAuthEvent);
});
services.AddScoped<CookieAuthEvent>();
services.AddControllersWithViews(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
Moreover, You don't need to retrieve data from the cookie as shown in Controller/UserProfile above.
Also, when I made debugging to check out the code I tested logout to make sure I really retrieve users data from the cookie not from UserManager and It really works well.
Here is the additional code of logOut
[Authorize]
public async Task<IActionResult> Logout()
{
await signInManager.SignOutAsync();
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return RedirectToAction("Index", "Home");
}

Dynamic database connection using Asp.Net identity

I am working on a multi-tenant application that uses multiple databases. There is one master database that contains user information and then each tenant database also has their own users for that tenant (which are a subset of the users in the master database).
The user will log in which will check the master database, then based on their details (i.e. which tenant they belong to) it will log them into the application using the user details on their tenant database.
I am using the method described in this thread (Dynamic database connection using Asp.net MVC and Identity2) to set the database for UserManager each time because at the point that the application starts it will not know what database to use therefore the following code in "Startup.Auth" would be setting the incorrect database:
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
This seems to be working well for most things but one problem I have is with the user getting logged out after the time set in "validateInterval" shown in the code below (this has been set to 20 seconds for testing):
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromSeconds(20),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)),
OnApplyRedirect = ctx =>
{
if (!IsAjaxRequest(ctx.Request))
{
ctx.Response.Redirect(ctx.RedirectUri);
}
}
}
});
I think the problem might be because when the code above is called in the "Startup.Auth" file it does not know what database to use however I have not confirmed this.
If I debug the "GenerateUserIdentityAsync" code I can see that it is getting the correct "securityStamp" for the user from the client database which makes me think it is finding the correct database but I cannot work out why it is still logging out the user after the time set for "validateInterval".
Can anyone offer any advice on how this can be resolved or at least possible ways to try and debug what the problem might be?
I have experienced the same issue on my multi-tenant ASP.NET MVC app.
If your goal is to set an expiration time for the logged-in user just remove the code in CookieAuthenticationProvider and set the ExpireTimeSpan property in the parent CookieAuthenticationOptions.
Your code should be:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
ExpireTimeSpan = TimeSpan.FromMinutes(15), //cookie expiration after 15 mins of user inactivity
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
}
});
Hope this helps.
Okay this is the full solution I have come up with which partly uses what #jacktric suggested but also allows for validating the security stamp if a users password has been changed elsewhere. Please let me know if anyone can recommend any improvements or see any downfalls in my solution.
I have removed the OnValidateIdentity section from the UseCookieAuthentication section as follows:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnApplyRedirect = ctx =>
{
if (!IsAjaxRequest(ctx.Request))
{
ctx.Response.Redirect(ctx.RedirectUri);
}
}
}
});
I then have the following IActionFilter that is registered in the FilterConfig.cs which checks if the user is logged in (I have parts of the system that can be accessed by anonymous users) and whether the current security stamp matches the one from the database. This check is made every 30 minutes using sessions to find out when the last check was.
public class CheckAuthenticationFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext filterContext)
{
}
public void OnActionExecuted(ActionExecutedContext filterContext)
{
try
{
// If not a child action, not an ajax request, not a RedirectResult and not a PartialViewResult
if (!filterContext.IsChildAction
&& !filterContext.HttpContext.Request.IsAjaxRequest()
&& !(filterContext.Result is RedirectResult)
&& !(filterContext.Result is PartialViewResult))
{
// Get current ID
string currentUserId = filterContext.HttpContext.User.Identity.GetUserId();
// If current user ID exists (i.e. it is not an anonymous function)
if (!String.IsNullOrEmpty(currentUserId))
{
// Variables
var lastValidateIdentityCheck = DateTime.MinValue;
var validateInterval = TimeSpan.FromMinutes(30);
var securityStampValid = true;
// Get instance of userManager
filterContext.HttpContext.GetOwinContext().Get<DbContext>().Database.Connection.ConnectionString = DbContext.GetConnectionString();
var userManager = filterContext.HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
// Find current user by ID
var currentUser = userManager.FindById(currentUserId);
// If "LastValidateIdentityCheck" session exists
if (HttpContext.Current.Session["LastValidateIdentityCheck"] != null)
DateTime.TryParse(HttpContext.Current.Session["LastValidateIdentityCheck"].ToString(), out lastValidateIdentityCheck);
// If first validation or validateInterval has passed
if (lastValidateIdentityCheck == DateTime.MinValue || DateTime.Now > lastValidateIdentityCheck.Add(validateInterval))
{
// Get current security stamp from logged in user
var currentSecurityStamp = filterContext.HttpContext.User.GetClaimValue("AspNet.Identity.SecurityStamp");
// Set whether security stamp valid
securityStampValid = currentUser != null && currentUser.SecurityStamp == currentSecurityStamp;
// Set LastValidateIdentityCheck session variable
HttpContext.Current.Session["LastValidateIdentityCheck"] = DateTime.Now;
}
// If current user doesn't exist or security stamp invalid then log them off
if (currentUser == null || !securityStampValid)
{
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary { { "Controller", "Account" }, { "Action", "LogOff" }, { "Area", "" } });
}
}
}
}
catch (Exception ex)
{
// Log error
}
}
}
I have the following extension methods for getting and updating claims for the logged in user (taken from this post https://stackoverflow.com/a/32112002/1806809):
public static void AddUpdateClaim(this IPrincipal currentPrincipal, string key, string value)
{
var identity = currentPrincipal.Identity as ClaimsIdentity;
if (identity == null)
return;
// Check for existing claim and remove it
var existingClaim = identity.FindFirst(key);
if (existingClaim != null)
identity.RemoveClaim(existingClaim);
// Add new claim
identity.AddClaim(new Claim(key, value));
// Set connection string - this overrides the default connection string set
// on "app.CreatePerOwinContext(DbContext.Create)" in "Startup.Auth.cs"
HttpContext.Current.GetOwinContext().Get<DbContext>().Database.Connection.ConnectionString = DbContext.GetConnectionString();
var authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
authenticationManager.AuthenticationResponseGrant = new AuthenticationResponseGrant(new ClaimsPrincipal(identity), new AuthenticationProperties() { IsPersistent = true });
}
public static string GetClaimValue(this IPrincipal currentPrincipal, string key)
{
var identity = currentPrincipal.Identity as ClaimsIdentity;
if (identity == null)
return null;
var claim = identity.Claims.FirstOrDefault(c => c.Type == key);
return claim.Value;
}
And finally anywhere that the users password is updated I call the following, this updates the security stamp for the user whose password is being edited and if it is the current logged in users password that is being edited then it updates the securityStamp claim for the current user so that they will not get logged out of their current session the next time the validity check is made:
// Update security stamp
UserManager.UpdateSecurityStamp(user.Id);
// If updating own password
if (GetCurrentUserId() == user.Id)
{
// Find current user by ID
var currentUser = UserManager.FindById(user.Id);
// Update logged in user security stamp (this is so their security stamp matches and they are not signed out the next time validity check is made in CheckAuthenticationFilter.cs)
User.AddUpdateClaim("AspNet.Identity.SecurityStamp", currentUser.SecurityStamp);
}

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.

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

MVC 5 OWIN - IsAuthenticated is false on external login (QQ Connect)

I hope someone can help me out with this problem - it's driving me mad! :)
I'm trying to use external login through QQ Connect (OAuth 2.0) using tinysnake's QQ Connect provider: https://github.com/tinysnake/microsoft-owin-security-qq
Everything seems to be going great - I can sign in via my QQ account and I get posted back to my ExternalLoginCallBack-method with the appropriate claims etc.
I use these values to sign the user in through the IAuthenticationManager - all goes well. However - when I redirect the user to another page and checks if he's logged in - then I get a false value from the IsAuthenticated value... and I can't read any of the claims I set earlier.
It might be a simple fix - but I just can't see it right now :)
Some code:
AuthConfig:
public static void ConfigureAuthentication(IAppBuilder app)
{
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Normal cookie sign in
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
AuthenticationMode = AuthenticationMode.Active
});
// QQ CONNECT
app.UseQQConnectAuthentication(
appId: "XXXXXX",
appSecret: "XXXXXXXXXXXXXXXXX");
}
AccountController:
//
// POST: /Account/ExternalLogin
[System.Web.Mvc.HttpPost]
[System.Web.Mvc.AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult ExternalLogin(string provider, string returnUrl)
{
// Request a redirect to the external login provider
return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }));
}
//
// GET: /Account/ExternalLoginCallback
[System.Web.Mvc.AllowAnonymous]
[HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)]
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var ctx = Request.GetOwinContext();
var result = ctx.Authentication.AuthenticateAsync(DefaultAuthenticationTypes.ExternalCookie).Result;
var claims = result.Identity.Claims.ToList();
var name = claims.First(i => i.Type == "urn:qqconnect:name");
claims.Add(new Claim(ClaimTypes.AuthenticationMethod, "QQ"));
claims.Add(new Claim(ClaimTypes.Name, name.Value));
var ci = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ExternalCookie);
ctx.Authentication.SignIn(ci);
// DO OTHER STUFF HERE
return Redirect("~/");
}
All seems to be going well so far...
HomeController:
public ActionResult Index()
{
var model = new HomeViewModel();
var ctx = Request.GetOwinContext();
if (ctx.Authentication.User.Identity.IsAuthenticated) // <-- THIS RETURNS FALSE
{
var claimsIdentity = User.Identity as ClaimsIdentity;
model.Name = claimsIdentity.FindFirst(ClaimTypes.Name).Value;
model.IsAuthenticated = true;
}
return View(model);
}
When I check the ctx.Authentication.User.Identity.IsAuthenticated, I get a false value... and I can't retrieve any of the claims either.
Am I missing something?
Any help would be greatly appreciated :)
UPDATE
I got my code working by doing this in my AccountController:
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var ctx = Request.GetOwinContext();
var result = ctx.Authentication.AuthenticateAsync(DefaultAuthenticationTypes.ExternalCookie).Result;
if (result.Identity.IsAuthenticated)
{
// Signed in successfully
var claims = result.Identity.Claims.ToList();
var name = claims.First(i => i.Type == "urn:qqconnect:name");
//claims.Add(new Claim(ClaimTypes.AuthenticationMethod, "QQ"));
claims.Add(new Claim(ClaimTypes.Name, name.Value));
var id = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);
var authenticationManager = ctx.Authentication;
authenticationManager.SignIn(id);
}
return Redirect("~/");
}
But the way I see it - here I'm using the ApplicationCookie and NOT the ExternalCookie for signing in... or am I missing something entirely?
This solution works for me - but I'd like to know if this is the right way to be doing this?
From my understanding, what you are experiencing is expected. Extremely oversimplifying:
The app gets the external information and uses it to create an external cookie
the external cookie is sent to your app with the assumption that it is just a temporary cookie that will be used to look up any additional local information about the user and then converted to a local [application] cookie
See UseCookieAuthentication vs. UseExternalSignInCookie for a somewhat more thorough breakdown.

Resources