Reputation: 125
I am using asp.net identity along with Identity Server 4 to log into my web site. The Identity code uses SQL server as its data store. When I add custom claims to the AspNetUserCliams table, log out and back in I do not see any of the values (old or new) in the ClaimsIdentity.Claims list. It does populate with a number of others that don't actually exist in that table:
How does one tell identity server to actually pull the associated claims from the table?
I've tried this code, but it's only when the object is in memory, doesn't persist the new claim to the database:
ClaimsIdentity id = new ClaimsIdentity();
id.AddClaim(new Claim("MyNewClaim", "bla"));
context.HttpContext.User.AddIdentity(id);
I've read a number of posts that talk about the elusive UserManager. However I'm not seeing anything inserted into the ServiceProvider that has the signature of UserManager<AspNetUser, Guid> or similar.
I'm using Microsoft.AspNetCore.Identity.EntityFrameworkCore, I would expect that to provide at least enough of a UserManager to persist and retrieve the data and allow me to override everything as needed. Does such an implementation already exist or do I have to reinvent the wheel and create a UserManager?
Update:
After a lot of cussing and searching I was able to create the UserManager instance with the following code:
public UserManager<AspNetUser> UserManager
{
get
{
var store = new UserStore<AspNetUser, AspNetRole, AuthorizationDbContext, Guid, AspNetUserClaim, AspNetUserRole, AspNetUserLogin, AspNetUserToken, AspNetRoleClaim>(Context);
var manager = new UserManager<AspNetUser>(
store,
null,
new PasswordHasher<AspNetUser>(),
new []{new UserValidator<AspNetUser>()},
new IPasswordValidator<AspNetUser>[]{},
new UpperInvariantLookupNormalizer(),
new IdentityErrorDescriber(),
null,
NullLogger<UserManager<AspNetUser>>.Instance);
return manager;
}
}
That allows me to update the datastore with the claim, but it's not being returned when I log in.
Using .Net Core 3.1, v3.10 of the Microsoft NuGet packages.
My Startup.cs has the following added:
.AddOpenIdConnect("oidc", options =>
{
options.Authority = Configuration.GetSection("Authentication")["Authority"];
options.ClientId = Configuration.GetSection("Authentication")["ClientId"];
options.ClientSecret = Configuration.GetSection("Authentication")["Secret"];
options.ResponseType = "code";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.MapAll();
options.Events.OnUserInformationReceived = async context =>
{
var mi = services.BuildServiceProvider().GetRequiredService<IModuleInfo>();
mi.ClearLoggedInUsersCache();
var userManager = services.BuildServiceProvider().GetRequiredService<UserManager<AspNetUser>>();
var userName = context.User.RootElement.GetString("name");
// Get the user object from the database.
var currentUser = (
from u in userManager.Users
where u.NormalizedUserName == userName
select u
).FirstOrDefault();
// Get the claims defined in the database.
var userClaims = await userManager.GetClaimsAsync(currentUser);
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-
// Just need to figure out how to attach them to the user.
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-
};
So now I can pull the claims using UserManager, but have no way to persist them to the user information to maintain across requests. Still missing a piece to the puzzle. But still seems odd that IdentityServer4 isn't returning the associated claims.
Upvotes: 0
Views: 3271
Reputation: 21716
ClaimsIdentity id = new ClaimsIdentity(); id.AddClaim(new Claim("MyNewClaim", "bla")); context.HttpContext.User.AddIdentity(id);
Hi @Ed Williams,
By using the above code, the new claim will add to the HttpContext, it will store data while processing a single request. The collection's contents are discarded after a request is processed.
To store the Claims in the AspNetUserClaims table, we could use the UserManager.AddClaimAsync() method to add the specified claim to the user. Check the following sample code:
Create a ClaimsController with the following code:
[Authorize]
public class ClaimsController : Controller
{
private UserManager<IdentityUser> userManager;
private SignInManager<IdentityUser> signInManager; //used to signin again and get the latest claims.
public ClaimsController(UserManager<IdentityUser> userMgr, SignInManager<IdentityUser> signMgr)
{
userManager = userMgr;
signInManager = signMgr;
}
public IActionResult Index()
{
return View(User?.Claims);
}
public IActionResult Create()
{
return View();
}
[HttpPost]
public async Task<IActionResult> CreateAsync(string claimType, string claimValue)
{
IdentityUser user = await userManager.GetUserAsync(HttpContext.User);
Claim claim = new Claim(claimType, claimValue, ClaimValueTypes.String);
IdentityResult result = await userManager.AddClaimAsync(user, claim);
HttpContext.User.Identities.FirstOrDefault().AddClaim(claim);
//signin again and get the latest claims.
await signInManager.SignInAsync(user, false, null);
if (result.Succeeded)
return RedirectToAction("Index");
else
Errors(result);
return View("Index", User.Claims);
}
[HttpPost]
public async Task<IActionResult> Delete(string claimValues)
{
IdentityUser user = await userManager.GetUserAsync(HttpContext.User);
string[] claimValuesArray = claimValues.Split(";");
string claimType = claimValuesArray[0], claimValue = claimValuesArray[1], claimIssuer = claimValuesArray[2];
Claim claim = User.Claims.Where(x => x.Type == claimType && x.Value == claimValue && x.Issuer == claimIssuer).FirstOrDefault();
IdentityResult result = await userManager.RemoveClaimAsync(user, claim);
await signInManager.SignInAsync(user, false, null);
if (result.Succeeded)
return RedirectToAction("Index");
else
Errors(result);
return View("Index", User.Claims);
}
void Errors(IdentityResult result)
{
foreach (IdentityError error in result.Errors)
ModelState.AddModelError("", error.Description);
}
}
Code in the Index page(Index.cshtml):
@model IEnumerable<System.Security.Claims.Claim>
<h2 class="bg-primary m-1 p-1 text-white">Claims</h2>
<a asp-action="Create" class="btn btn-secondary">Create a Claim</a>
<table class="table table-sm table-bordered">
<tr>
<th>Subject</th>
<th>Issuer</th>
<th>Type</th>
<th>Value</th>
<th>Delete</th>
</tr>
@foreach (var claim in Model.OrderBy(x => x.Type))
{
<tr>
<td>@claim.Subject.Name</td>
<td>@claim.Issuer</td>
<td>@claim.Type</td>
<td>@claim.Value</td>
<td>
<form asp-action="Delete" method="post">
<input type="hidden" name="claimValues" value="@claim.Type;@claim.Value;@claim.Issuer" />
<button type="submit" class="btn btn-sm btn-danger">
Delete
</button>
</form>
</td>
</tr>
}
</table>
Code in the Create page(Create.cshtml):
@model System.Security.Claims.Claim
@{
ViewData["Title"] = "Create";
}
<h1>Create</h1>
<h1 class="bg-info text-white">Create Claim</h1>
<a asp-action="Index" class="btn btn-secondary">Back</a>
<div asp-validation-summary="All" class="text-danger"></div>
<form asp-action="Create" asp-controller="Claims" method="post">
<div class="form-group">
<label for="ClaimType">Claim Type:</label>
<input name="ClaimType" class="form-control" />
</div>
<div class="form-group">
<label for="ClaimValue">Claim Value:</label>
<input name="ClaimValue" class="form-control" />
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
[Note] By using the above code, after adding or removing claim to the user, we have to refresh the current user, in this sample, I'm using the SignInManager.SignInAsync() method to sign in again and update the claims.
Then, the screenshot as below:
Edit:
About the UserManager, it is derived from the Microsoft.AspNetCore.Identity and Microsoft.Extensions.Identity.Core.dll.
In Asp.Net Core 3.1+ version application, after configure the Identity and database use the following code in the Startup.ConfigureServices (Here you might need to install the EntityFrameWork package, in my sample I installed the these packages):
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<IdentityUser, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = true)
.AddDefaultUI()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddControllersWithViews().AddJsonOptions(opts =>
{
opts.JsonSerializerOptions.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
});
services.AddRazorPages();
}
The ApplicationDbContext
inherits from IdentityDbContext
public class ApplicationDbContext : IdentityDbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
[Note] You might need to use Migration to generate the database.
After that, in the controller, we could use Dependency injection to register the UserManager, code as below:
public class ClaimsController : Controller
{
private readonly UserManager<IdentityUser> userManager;
private readonly SignInManager<IdentityUser> signInManager; //used to signin again and get the latest claims.
public ClaimsController(UserManager<IdentityUser> userMgr, SignInManager<IdentityUser> signMgr)
{
userManager = userMgr;
signInManager = signMgr;
}
More detail information, you could check the following articles:
Scaffold Identity in ASP.NET Core projects
Finally, if still can't use UserManager, as a workaround, you could directly access the AspNetUserClaims table via the dbcontext, then use the EF core to manage user's claims. Please refer the following code:
[Authorize]
public class ClaimsController : Controller
{
private readonly UserManager<IdentityUser> userManager;
private readonly SignInManager<IdentityUser> signInManager; //used to signin again and get the latest claims.
private readonly ApplicationDbContext _dbcontext;
public ClaimsController(UserManager<IdentityUser> userMgr, SignInManager<IdentityUser> signMgr, ApplicationDbContext context)
{
userManager = userMgr;
signInManager = signMgr;
_dbcontext = context;
}
public IActionResult Index()
{
// access the UserClaims table and get the User's claims
var claims = _dbcontext.UserClaims.ToList();
//loop through the claims
//then based on the resut to creat claims
//Claim claim = new Claim(claimType, claimValue, ClaimValueTypes.String);
//and using the following code to add claim to current user.
HttpContext.User.Identities.FirstOrDefault().AddClaim(claim);
return View(HttpContext.User?.Claims);
}
To delete claim from current user, you could try to use the following code:
HttpContext.User.Identities.FirstOrDefault().RemoveClaim(claim);
Upvotes: 2