Transactions with ASP.NET Core 1.0 Identity UserManager - asp.net

In one of my apps, I'm rewriting Asp Core Identity UserManager's CreateAsync, to in addition to creating new user in UserStore - create a new row in a separate statistics table (on a different dbcontext). Problem is, I would like both of those operations to be fired inside a transaction, so that if one fails - the other does not commit. Here's the code :
public override async Task<IdentityResult> CreateAsync(TUser user, string password)
{
// We need to use transactions here.
var result = await base.CreateAsync(user, password);
if (result.Succeeded)
{
var appUser = user as IdentityUser;
if (appUser != null)
{
// create user stats.
var userStats = new UserStats()
{
ActionsCount = 0,
EventsCount = 0,
FollowersCount = 0,
ProjectsCount = 0,
UserId = appUser.Id
};
_ctx.UserStats.Add(userStats);
await _ctx.SaveChangesAsync();
}
}
return result;
}
Thing is, I have no idea how to set up such a transaction, as it seems TransactionScope is not a part of .Net Core yet (?) and getting currently scoped DbContext for base.CreateAsync() with HttpContext.GetOwinContext() does not work as HttpContext seems to be missing this method (referencing Microsoft.Owin.Host.SystemWeb or Microsoft.AspNet.WebApi.Owin as hinted in some other stack overflow answers won't do - both are not compatible with .netcore). Any help ?

Firstly you need to setup the dbcontext you are using with identity with a scoped lifetime:
services.AddDbContext<MyDbContext>(ServiceLifetime.Scoped); // this is the important bit
services.AddIdentity<User, Role>(options =>
{
})
.AddEntityFrameworkStores<MyDbContext, int>()
.AddDefaultTokenProviders();
Then when you need to create a transaction you do the following:
using (var transaction = await _myDbContext.Database.BeginTransactionAsync())
{
var result = await _userManager.CreateAsync(newUser, password);
try
{
if (result.Succeeded)
{
DBItem newItem = new DBItem();
_myDbContext.Add(newItem);
await _myDbContext.SaveChangesAsync();
transaction.Commit();
}
}
catch (Exception e)
{
transaction.Rollback();
}
}

Related

How to pass connection string name from Silverlight Application to DomainService

First I want to say I'm not SL developer. I just need to modify one legacy Silverlight 5 application.
It is using RIA services and XAP is hosted in Asp.Net page.
User on login page enters credentials and is able to select database from dropdown. Whole web is using multiple connections and user is able to select database to connect.
This selected database (or any identificator for data connection) is sent do XAP's InitParams, so I can access it from SL.
private void Application_Startup(object sender, StartupEventArgs e)
{
foreach (var item in e.InitParams)
{
Resources.Add(item.Key, item.Value);
}
var selectedConnectionString = GetInitParam("ConnectionString");
// TODO: Different way to store connection string
SetCookie("ConnectionString", selectedConnectionString);
RootVisual = new LoadingPage();
}
Currently I'm trying to use cookie to store selected database. I found it somewhere as one possible solution. But it needs to change.
Ok, then we have DomainService.
public class CommissionDomainService : LinqToEntitiesDomainService<CommissionEntitiesContext>
{
...
}
I know that I need to use CreateObjectContext to change ConnectionString in service. So I have:
protected override CommissionEntitiesContext CreateObjectContext()
{
// TODO: Different way to store connection string
string connectionStringName;
if (System.Web.HttpContext.Current.Request.Cookies["ConnectionString"] != null)
{
connectionStringName = System.Web.HttpContext.Current.Request.Cookies["ConnectionString"].Value;
}
else
{
throw new Exception("Missing connectionStringName");
}
var connectionStringSettings = ConfigurationManager.ConnectionStrings[connectionStringName];
var entityCs = new EntityConnectionStringBuilder
{
Metadata = "res://*/CommissionEntities.csdl|res://*/CommissionEntities.ssdl|res://*/CommissionEntities.msl",
Provider = connectionStringSettings.ProviderName,
ProviderConnectionString = connectionStringSettings.ConnectionString
};
return new CommissionEntitiesContext(entityCs.ConnectionString);
}
Again, I used Cookie to pass value from application to service.
But it is not the best idea. Because of cookie and because of persistance etc.
My question is, how to pass my ConnectionString value from main application to DomainService? Or Can I access some application context from service? Or maybe can I get connection string somewhere in EntitiesContext?
Ok, I did it this way.
I made selected database part of user identity. Because I'm using Owin, I just used one of Claims.
So when user logs in, I just put one claim with selected database
// build a list of claims
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.Name),
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.UserData, selectedDatabase)
};
// create the identity
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationType);
// sign in
Context.GetOwinContext().Authentication.SignIn(new AuthenticationProperties { IsPersistent = false }, identity);
Then in DomainService I used Initialize and CreateObjectContext methods
private string _connectionStringName;
public override void Initialize(DomainServiceContext context)
{
// Načteme z kontextu usera zvolenou databázi
var claim = ((ClaimsIdentity)context.User.Identity).FindFirst(ClaimTypes.UserData);
_connectionStringName = claim.Value;
base.Initialize(context);
...
}
protected override CommissionEntitiesContext CreateObjectContext()
{
if (string.IsNullOrEmpty(_connectionStringName))
{
throw new Exception("Missing connectionStringName");
}
var connectionStringSettings = ConfigurationManager.ConnectionStrings[_connectionStringName];
var entityCs = new EntityConnectionStringBuilder
{
Metadata = "res://*/CommissionEntities.csdl|res://*/CommissionEntities.ssdl|res://*/CommissionEntities.msl",
Provider = connectionStringSettings.ProviderName,
ProviderConnectionString = connectionStringSettings.ConnectionString
};
return new CommissionEntitiesContext(entityCs.ConnectionString);
}

How to refresh claim of a different user than the one logged in during the current request?

I use the following code to update the claims of a user in my web application. However, to update the claims/cookie of this user, I want to force him to login again. So essentially I want to expire his cookie after I update the claims. Any idea how to do this?
await _signInManager.RefreshSignInAsync(user); is the first thing I tried, but fails because I'm updating the claims of another user (the one that is currently logged in) :)
All other examples I found are more or less the same as RefreshSignInAsync and do not deal with the fact that I'm updating the claims of another user.
public async Task<IActionResult> AddClaimPost(string id)
{
var user = _context.ApplicationUser
.SingleOrDefault(m => m.Id == id);
foreach(var item in Request.Form)
{
if (item.Key.Contains("Claim"))
{
if (item.Value.Contains("true"))
{
if (!User.HasClaim(item.Key, item.Key))
{
var result = await _userManager.AddClaimAsync(user, new Claim(item.Key, item.Key));
}
}
else
{
var result2 = await _userManager.RemoveClaimAsync(user, new Claim(item.Key, item.Key));
}
}
}
await _signInManager.RefreshSignInAsync(user);
return RedirectToAction("Overview");
}
After searching a few days I discovered that what I want is not possible. You cannot force logging the user out without putting the cookie timespan to 0
options.Cookies.ApplicationCookie.ExpireTimeSpan = 0;
In this case it will check the cookie every time the user makes a request. With the following code you can than force the user to login again:
await _userManager.UpdateSecurityStampAsync(user);
I don't recommend the 0 expire timespan approach.
If you have a redis server (or any other persistent data store that is performant) you can do something like:
await redis.StringSetAsync("refresh_login_" + user.Id, "1", null);
Then on every page load you will check this redis value and refresh the signin if the key is set for you:
Filters/BaseActionFilter.cs:
public class BaseActionFilter: IAsyncActionFilter, IAsyncPageFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{ // For classic controllers
await PerformPageTasks(context.HttpContext);
await next();
}
public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context,
PageHandlerExecutionDelegate next)
{ // For razor pages in Areas/
if ((context.HandlerInstance is PageModel page))
{
await PerformPageTasks(context.HttpContext);
}
await next.Invoke();
}
public async Task PerformPageTasks(HttpContext context)
{
var signinManager = context.RequestServices.GetService<SignInManager<MyWebUser>>();
if (signinManager.IsSignedIn(context.User))
{
var cache = context.RequestServices.GetService<IDistributedCache>();
var redis = (await ((RedisCache)cache).GetConnectionAsync()).GetDatabase();
var userManager = context.RequestServices.GetService<UserManager<MyWebUser>>();
var user = await userManager.GetUserAsync(context.User);
if ((await redis.StringGetAsync("refresh_login_" + user.Id)) == "1")
{
await redis.KeyDeleteAsync("refresh_login_" + user.Id);
// refresh the user
await signinManager.RefreshSignInAsync(user);
}
}
}
public async Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
{
await Task.CompletedTask;
}
}
Startup.cs:
services.AddMvc(o =>
{
...
o.Filters.Add(new BaseActionFilter());
}).AddHybridModelBinder();
If you only use traditional controllers or Areas/ razor pages then you can adapt the code accordingly.
Note this requires the user to make an additional page load before the claims are set, so for things like [Authorize] you would need to put this code earlier in the chain and I'm not sure exactly how to do that.

ASP.Net Identity. UserManager.CreateAsync Throws Error: Value cannot be null. Parameter name: request

I am trying to create a user by calling register function within the application.
This function works fine when the function is called as a API.
When Called from Inside the application it throws an error
AccountController ac = new AccountController();
RegisterBindingModel rbm = new RegisterBindingModel();
rbm.Email = UserAccountBase.Email;
rbm.Password = "TestPassword";
rbm.ConfirmPassword = "TestPassword";
var userId = await ac.Register(rbm);
// POST api/Account/Register
[AllowAnonymous]
[Route("Register")]
[ApiExplorerSettings(IgnoreApi = true)]
public async Task<IHttpActionResult> Register(RegisterBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var user = new ApplicationUser() { UserName = model.Email, Email = model.Email };
try
{
var result = await UserManager.CreateAsync(user, model.Password);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
}
catch(Exception e) {
var message = e.Message;
}
return Ok(user.Id);
}
UserManager.CreateAsync Throws an error
Value cannot be null. Parameter name: request
Did you build this using ASP Identity 2.0 and EF Code First? If so, check to see if your initial DB configuration put a column called 'Discriminator' at the end of AspNetUsers. If this is the case, recreate an initial migration and remove that column that gets generated. Once removed, you can perform an update-database. Does this make any sense? I just had this happen to me.
In the identity model, you will have a Discriminator column. In your model, add migration and update the database.

usermanager.addtorole - An asynchronous module or handler completed while an asynchronous operation was still pending

I am adding a user to my aspnetusers database and that is working fine. Then I am also trying to link them to an existing role. That is when I get the error: "An asynchronous module or handler completed while an asynchronous operation was still pending."
Here is my method with the problem code:
private async void checkOldDB(string email, string password)
{
bool isValidUser = false;
ReportsMvc.App_Code.BLL.FUN.cFunUser user = ReportsMvc.App_Code.DAL.FUN.cFunUserDB.getUser(email);
if (user != null)
{
isValidUser = PasswordHash.PasswordHash.ValidatePassword(password, user.Password);
if (!isValidUser)
{
isValidUser = PasswordHash.PasswordHash.ValidateHashes(password, user.Password);
}
}
if (isValidUser)
{
var user2 = new ApplicationUser { UserName = email, Email = email };
var result = await UserManager.CreateAsync(user2, password);
if (result.Succeeded)
{
string role = user.Role;
if (string.IsNullOrEmpty(role))
{
role = "User";
}
UserManager.AddToRole(user2.Id, role);
await SignInManager.SignInAsync(user2, isPersistent: false, rememberBrowser: false);
}
}
}
The line starting with "await SignInManager" was working fine. Then when I added in that code to AddToRole, I started getting the above error. This identity/authentication stuff is all very new to me.
You should change async void to async Task and await it where you call it.
As a general rule, you should avoid async void; it should only be used for event handlers. I describe this more in an MSDN article.

How do I access Microsoft.Owin.Security.xyz OnAuthenticated context AddClaims values?

I'm trying to retrieve user properties that are returned as the OnAuthenticated context and added as a claims following this example: How to access Facebook private information by using ASP.NET Identity (OWIN)?
I can see that data I am expecting is being returned at login and is being added as a Claim within Starup.Auth.cs. But, when I am within the Account Controller, the only claims that appears within the UserManager or UserStore is issued by LOCAL AUTHORITY. No claims can be found for Facebook (or other external providers). Where do the claims added to context end up? (I'm using VS2013 RTM.)
Full source and live site on Azure linked here: https://github.com/johndpalm/IdentityUserPropertiesSample/tree/VS2013rtm
Here is what I have in Startup.Auth.cs:
var facebookOptions = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationOptions()
{
AppId = ConfigurationManager.AppSettings.Get("FacebookAppId"),
AppSecret = ConfigurationManager.AppSettings.Get("FacebookAppSecret"),
Provider = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationProvider()
{
OnAuthenticated = (context) =>
{
const string XmlSchemaString = "http://www.w3.org/2001/XMLSchema#string";
foreach (var x in context.User)
{
var claimType = string.Format("urn:facebook:{0}", x.Key);
string claimValue = x.Value.ToString();
if (!context.Identity.HasClaim(claimType, claimValue))
context.Identity.AddClaim(new System.Security.Claims.Claim(claimType, claimValue, XmlSchemaString, "Facebook"));
}
context.Identity.AddClaim(new System.Security.Claims.Claim("urn:facebook:access_token", context.AccessToken, XmlSchemaString, "Facebook"));
return Task.FromResult(0);
}
}
};
facebookOptions.Scope.Add("email");
app.UseFacebookAuthentication(facebookOptions);
An alternative way to capture the external login properties would be to add a single claim for the access token and populate it with properties:
const string XmlSchemaString = "http://www.w3.org/2001/XMLSchema#string";
var facebookOptions = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationOptions
{
AppId = ConfigurationManager.AppSettings.Get("FacebookAppId"),
AppSecret = ConfigurationManager.AppSettings.Get("FacebookAppSecret"),
Provider = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationProvider()
{
OnAuthenticated = (context) =>
{
var claim = new System.Security.Claims.Claim("urn:facebook:access_token", context.AccessToken, XmlSchemaString, "Facebook");
foreach (var x in context.User)
{
string key = string.Format("urn:facebook:{0}", x.Key);
string value = x.Value.ToString();
claim.Properties.Add(key, value);
}
context.Identity.AddClaim(claim);
return Task.FromResult(0);
}
}
};
NOTE - This sample does not work: Though it would be nice to pass a single claim with properties. The external cookie seems to note honor the claims properties. The properties are empty when retrieving them later from the identity.
I was able to create a working example, using MVC 5 RTM templates, OWIN, and ASP.NET Identity bits. You can find the complete source and a link to a live working example here: https://github.com/johndpalm/IdentityUserPropertiesSample
Here's what worked for me:
Create a new (insert provider name here) AuthenticationOptions object in Startup.ConfigureAuth (StartupAuth.cs), passing it the client id, client secret, and a new AuthenticationProvider. You will use a lambda expression to pass the OnAuthenticated method some code to add Claims to the identity which contain the values you extract from context.Identity.
StartUp.Auth.cs
// Facebook : Create New App
// https://dev.twitter.com/apps
if (ConfigurationManager.AppSettings.Get("FacebookAppId").Length > 0)
{
var facebookOptions = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationOptions()
{
AppId = ConfigurationManager.AppSettings.Get("FacebookAppId"),
AppSecret = ConfigurationManager.AppSettings.Get("FacebookAppSecret"),
Provider = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationProvider()
{
OnAuthenticated = (context) =>
{
context.Identity.AddClaim(new System.Security.Claims.Claim("urn:facebook:access_token", context.AccessToken, XmlSchemaString, "Facebook"));
foreach (var x in context.User)
{
var claimType = string.Format("urn:facebook:{0}", x.Key);
string claimValue = x.Value.ToString();
if (!context.Identity.HasClaim(claimType, claimValue))
context.Identity.AddClaim(new System.Security.Claims.Claim(claimType, claimValue, XmlSchemaString, "Facebook"));
}
return Task.FromResult(0);
}
}
};
app.UseFacebookAuthentication(facebookOptions);
}
NOTE: The Facebook auth provider works with the code used here. If you use this same code with the Microsoft Account provider (or Foursquare provider I created using the MS account code as a model), it fails to login. If you select just the access_token parameter, it works fine. Seems like some parameters break the login process. (An issue has been opened on katanaproject.codeplex.com if progress on this is of interest to you.) I'll update if I find the cause. I didn't do much with Twitter or Google beyond verifying that I could get the access_token.
var msaccountOptions = new Microsoft.Owin.Security.MicrosoftAccount.MicrosoftAccountAuthenticationOptions()
{
ClientId = ConfigurationManager.AppSettings.Get("MicrosoftClientId"),
ClientSecret = ConfigurationManager.AppSettings.Get("MicrosoftClientSecret"),
Provider = new Microsoft.Owin.Security.MicrosoftAccount.MicrosoftAccountAuthenticationProvider()
{
OnAuthenticated = (context) =>
{
context.Identity.AddClaim(new System.Security.Claims.Claim("urn:microsoftaccount:access_token", context.AccessToken, XmlSchemaString, "Microsoft"));
return Task.FromResult(0);
}
}
};
app.UseMicrosoftAccountAuthentication(msaccountOptions);
In AccountController, I extract the ClaimsIdentity from the AuthenticationManager using the external cookie. I then add it to the identity created using the application cookie. I ignored any claims that starts with "...schemas.xmlsoap.org/ws/2005/05/identity/claims" since it seemed to break the login.
AccountController.cs
private async Task SignInAsync(CustomUser user, bool isPersistent)
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
// Extracted the part that has been changed in SignInAsync for clarity.
await SetExternalProperties(identity);
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}
private async Task SetExternalProperties(ClaimsIdentity identity)
{
// get external claims captured in Startup.ConfigureAuth
ClaimsIdentity ext = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);
if (ext != null)
{
var ignoreClaim = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims";
// add external claims to identity
foreach (var c in ext.Claims)
{
if (!c.Type.StartsWith(ignoreClaim))
if (!identity.HasClaim(c.Type, c.Value))
identity.AddClaim(c);
}
}
}
And finally, I want to display whatever values are not from the LOCAL AUTHORITY. I created a partial view _ExternalUserPropertiesListPartial that appears on the /Account/Manage page. I get the claims I previously stored from AuthenticationManager.User.Claims and then pass it to the view.
AccountController.cs
[ChildActionOnly]
public ActionResult ExternalUserPropertiesList()
{
var extList = GetExternalProperties();
return (ActionResult)PartialView("_ExternalUserPropertiesListPartial", extList);
}
private List<ExtPropertyViewModel> GetExternalProperties()
{
var claimlist = from claims in AuthenticationManager.User.Claims
where claims.Issuer != "LOCAL AUTHORITY"
select new ExtPropertyViewModel
{
Issuer = claims.Issuer,
Type = claims.Type,
Value = claims.Value
};
return claimlist.ToList<ExtPropertyViewModel>();
}
And just to be thorough, the view:
_ExternalUserPropertiesListPartial.cshtml
#model IEnumerable<MySample.Models.ExtPropertyViewModel>
#if (Model != null)
{
<legend>External User Properties</legend>
<table class="table">
<tbody>
#foreach (var claim in Model)
{
<tr>
<td>#claim.Issuer</td>
<td>#claim.Type</td>
<td>#claim.Value</td>
</tr>
}
</tbody>
</table>
}
Again, the working example and complete code is on GitHub: https://github.com/johndpalm/IdentityUserPropertiesSample
And any feedback, corrections, or improvements would be appreciated.
So this article explains how this all works pretty well: Decoupling owin external auth
But the short answer is, when you get authenticated from facebook, that is giving you an external identity. You then need to take that external identity and 'sign in' a local app identity, its in that stepthat you need to add any claims you want from the external identity to the ClaimsIdentity that becomes User.Identity.
Edit: To clarify further, you could do it inside of ExternalLoginCallback:
// GET: /Account/ExternalLoginCallback
[AllowAnonymous]
public async Task<ActionResult> ExternalLoginCallback(string returnUrl) {
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null) {
return RedirectToAction("Login");
}
// Sign in this external identity if its already linked
var user = await UserManager.FindAsync(loginInfo.Login);
if (user != null) {
await SignInAsync(user, isPersistent: false);
return RedirectToLocal(returnUrl);
}
private async Task SignInAsync(ApplicationUser user, bool isPersistent) {
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}
So you will need to pass in extra data to the SignIn, which will look something like this:
ClaimsIdentity id = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);
This ClaimsIdentity will have your added claim, and you will need to add that claim to the identity created in the SignInAsync method for it to show up.
In short the line that is required once AddClaim is used is as follows:
Taken from johns answer above.
ClaimsIdentity ext = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);

Resources