I'm looking to create an ASP.NET 6 Core Identity solution where users can create groups.
The creator becomes admin of that group and can Crud other users to the group - (the other users may then become an admin of the group).
The creator also needs to exist in other groups with and without admin.
Is there an easy way to do this in Identity using roles as groups with policies/claims or are some new tables required? like this
https://www.codeproject.com/Tips/5276188/Implementing-User-Groups-using-Claims-in-ASP-NET-I?fid=1962554&df=90&mpp=25&sort=Position&view=Normal&spc=Relaxed&prof=True
Using claims by itself is enough for authorization (e.g. is this user an admin, can he promote this other user to admin status). But using claims limits you to string types, which could take arbitrary values. This puts the burden on you for making sure every value is as it's supposed to be.
To have a more sound DB schema, you should store memberships in a separate table. This means creating a many-to-many relation between User & Group to store membership, which would necessitate a lookup table, but seeing how you're using ASP.NET Core 5, and likely EF Core 5, it creates this table for you.
But likely you'd need to store when and by whom a user has been added to a group. This would mean storing some data on the lookup table. So if you need those, you should create an entity for that lookup table too.
As for the code, here's a starting point for you:
public record Group(string Name)
{
public const string AdminClaimName = "group_admin";
public Guid Id { get; init; } = Guid.Empty;
public List<IdentityUser> Members { get; set; } = new List<IdentityUser>();
}
[ApiController]
public class UserGroupController : ControllerBase
{
private readonly UserManager<IdentityUser> _userManager;
private readonly ApplicationDbContext _db;
public UserGroupController(UserManager<IdentityUser> userManager, ApplicationDbContext db)
{
_userManager = userManager;
_db = db;
}
public record GroupCreateRequest(string GroupName);
[HttpPost]
public async Task<IActionResult> CreateGroup(GroupCreateRequest request,
CancellationToken cancellationToken = default)
{
var user = await _userManager.GetUserAsync(User);
var group = new Group(request.GroupName);
await _db.Set<Group>().AddAsync(group, cancellationToken);
await _db.SaveChangesAsync(cancellationToken);
await _userManager.AddClaimAsync(user, new Claim(type: Group.AdminClaimName, value: group.Id.ToString()));
return Ok(group);
}
public record GroupMembershipRequest(string MemberUserId);
[HttpPost("{id:guid}/membership")]
public async Task<IActionResult> AddUserToGroup(Guid groupId,
GroupMembershipRequest request,
CancellationToken cancellationToken = default)
{
var user = await _userManager.GetUserAsync(User);
var claims = await _userManager.GetClaimsAsync(user);
// check if the current user is the admin of that group
if (claims.Any(c => c.Type == Group.AdminClaimName && c.Value == groupId.ToString()))
{
return Forbid();
}
var member = await _userManager.FindByIdAsync(request.MemberUserId);
var group = await _db.Set<Group>().FindAsync(groupId);
group.Members.Add(member);
await _db.SaveChangesAsync(cancellationToken);
return NoContent();
}
public record UserPromotionRequest(Guid GroupId, string MemberUserId);
[HttpPost("promote")]
public async Task<IActionResult> PromoteUserToAdmin(UserPromotionRequest request,
CancellationToken cancellationToken = default)
{
var user = await _userManager.GetUserAsync(User);
var claims = await _userManager.GetClaimsAsync(user);
// check if the current user is the admin of that group
if (claims.Any(c => c.Type == Group.AdminClaimName && c.Value == request.GroupId.ToString()))
{
return Forbid();
}
var member = await _userManager.FindByIdAsync(request.MemberUserId);
var group = await _db.Set<Group>().FindAsync(request.GroupId);
await _userManager.AddClaimAsync(member, new Claim(type: Group.AdminClaimName, value: group.Id.ToString()));
return NoContent();
}
}
(I've omitted some validations & null checks, you need to add those yourself.)
Notice the difference between User property of the controller (the user currently logged in) and user instance fetched from the database.
Related
I have a weird situation which I don't understand. While testing my API I noticed slow api queries which boils down to retrieving the user from the store (after login).
When I override the FindByIdAsync method in my UserStore I can see a delay of 500ms when retrieving the User from the DbContext.
public override async Task<User> FindByIdAsync(string userId, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (!int.TryParse(userId, out int id))
{
return null;
}
// This takes 500+ ms
return Context.User.FirstOrDefault(u => u.Id == id);
}
Now the weird thing is when I do the same in a controller it is fast.
For example:
[HttpGet]
public async Task<IActionResult> Get()
{
// This function will end up at the UserStore.FindByIdAsync (see above)
// And takes 500+ ms
User user = await signInManager.UserManager.GetUserAsync(this.User);
// This however is fast... (just using a sample id)
context.User.Where(u => u.Id == 1896);
return Ok(await context.Session.Where(s => s.UserId == user.Id).ToListAsync());
}
I do not understand why this is. I tried swapping the two functions to see whether it was a warmup thing or something. But that is not the case..
I also looking in the source code from the UserStore and the Context there should be the same as the context in the Controller
I inject the context in the controller:
public SessionController(SignInManager<User> signInManager, MyDbContext context)
{
this.signInManager = signInManager;
// This is the same context as in the UserStore since the context is also injected in the UserStore
this.context = context;
}
Add ConfigureAwait(false) at the await call
User user = await signInManager.UserManager.GetUserAsync(this.User).ConfigureAwait(false);
await context.Session.Where(s => s.UserId == user.Id).ToListAsync().ConfigureAwait(false)
I have an application which requires role authorization using custom database. The database is set up with a tblUsers table that has a reference to a tblRoles table. The users are also already assigned to their roles.
I also want to use the [Authorize(Role = "RoleName")] attribute on each action to check if an authenticated user is assigned to "RoleName" in the database. I'm having a lot of trouble figuring out where I need to make a modification to the [Authorize] attribute so it behaves that way. I just want to see if a username has a role, I won't have a page to manage roles in the database.
I have tried implementing custom storage providers for ASP.NET Core Identity, but it's starting to look like this is not what I need because I'm not gonna be managing roles within the application, and I can't tell how it affects the behavior of [Authorize] attribute.
Also, it's likely that I have a false assumption in my understanding on how the [Authorize] attribute even works. If you notice it, I would appreciate if you could point it out.
I had a similar problem when my client asked for granular permissions for each role. I couldn't find a way to modify the Authorize attribute but was able to implement the solution with a custom attribute. But it depends on one thing i.e can you get the userId of the calling user? I used cookie authentication so I just include the userId in my claims when someone logs in so when a request comes I can always get it from there. I think the built-in Session logic in asp.net might get the job done too, I can't say for sure though. Anyways the logic for custom authorization goes like this:
Load users and roles from database to cache on startup. If you haven't set up a cache in your program (and don't want to) you can simply make your own for this purpose by making a UserRoleCache class with 2 static lists in it. Also there are several ways of loading data from db on startup but I found it easy to do that directly in Program.cs as you'll see below.
Define your custom attribute to check if the calling user has the required role by iterating over lists in cache and return 403 if not.
Modify your Program class like:
public class Program
{
public static async Task Main(string[] args)
{
IWebHost webHost = CreateWebHostBuilder(args).Build();
using (var scope = webHost.Services.CreateScope())
{
//Get the DbContext instance. Replace MyDbContext with the
//actual name of the context in your program
var context = scope.ServiceProvider.GetRequiredService<MyDbContext>();
List<User> users = await context.User.ToListAsync();
List<Role> roles = await context.Role.ToListAsync();
//You may make getters and setters, this is just to give you an idea
UserRoleCache.users = users;
UserRoleCache.roles = roles;
}
webHost.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
Then comes the logic for checking if user has a role. Notice I've used an array of roles because sometimes you'll want to allow access to multiple roles.
public class RoleRequirementFilter : IAuthorizationFilter
{
private readonly string[] _roles;
public PermissionRequirementFilter(string[] roles)
{
_roles = roles;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
bool hasRole = false;
//Assuming there's a way you can get the userId
var userId = GetUserId();
User user = UserRoleCache.users.FirstOrDefault(x => x.Id == userId);
//Where roleType is the name of the role like Admin, Manager etc
List<Role> roles = UserRoleCache.roles.FindAll(x => _roles.Contains(x.RoleType))
foreach(var role in roles)
{
if(user.RoleId == role.Id)
{
hasRole = true;
break;
}
}
if (!hasRole)
context.Result = new StatusCodeResult(403);
}
}
Finally make the Role attribute
public class RoleAttribute : TypeFilterAttribute
{
public RoleAttribute(params string[] roles) : base(typeof(RoleRequirementFilter))
{
Arguments = new object[] { roles };
}
}
Now you can use the Role attribute in your controllers:
public class SampleController : ControllerBase
{
[HttpGet]
[Role("Admin", "Manager")]
public async Task<ActionResult> Get()
{
}
[HttpPost]
[Role("Admin")]
public async Task<ActionResult> Post()
{
}
}
I want administrators to be able to assign AspNetUser to a specific customer. The relationship is supposed to be that multiple logins (AspNetUsers) have a foreign key to the same customer. To do so i added the following property to my identityModel.
public virtual Customer Customer { get; set; }
The column is added to the db; so far so good.
When I try to update the ApplicationUser entity with an instance of an ApplicationUserManager no exception is thrown and the view is returned. Nonetheless no changes to the db. When debugging i see that the customer gets set for the user, so I'm wondering if there is anything I'm missing?
public async Task<ActionResult> Edit([Bind(Include = "Id,Email,Customer")] ApplicationUser user)
{
if (ModelState.IsValid)
{
ApplicationUserManager applicationUserManager = new ApplicationUserManager(new UserStore<ApplicationUser>(db));
var customerName = Request.Form["customerSelect"];
var customer = db.Customers.SingleOrDefault(c => c.Name == customerName);
user.Customer = customer;
await applicationUserManager.UpdateAsync(user);
return RedirectToAction("index");
}
return View(user);
}
I thought of creating a new entity that maps AspNetUser.Id to Customer.Id, but i would prefer not to.
Hello, I'm starting with private messages between ASP.net users, using SignalR, everything Works fine if I'm using ConnectionId - s, In this way I can take recipient's ConnectionId and send private message to this person. Now about problem, I want store messages in database and load them on login, I'm using Standard membership of ASP.net mvc5 application, so after reconnect ConnectionId is changing. I was reading article Mapping SignalR Users to Connections, I but cannot understand how to use IUserProvider Can you explain me how to make this taks. lot of thanks.
Here is my hub code:
[HubName("chatHub")]
public class ChatHub : Hub
{
static List<ApplicationUser> Users = new List<ApplicationUser>();
private ApplicationDbContext db = new ApplicationDbContext();
public void Send(string name, string message)
{
Clients.All.addMessage(name, message);
}
public void SendPM(string name, string privatemessage, string userid)
{
//This line not Works I've commented it but filling It's correct way
//Clients.User(userid).addPM(name, privatemessage);
Clients.Client(userid).addPM(name, privatemessage);
}
public void Connect(string userName)
{
var id = Context.ConnectionId;
var appuser = db.Users.FirstOrDefault(x => x.UserName == userName);
var dbUsers = db.Users.ToList();
if (!Users.Any(x => x.ConnectionId == id))
{
Users.Add(new ApplicationUser { ConnectionId = id, UserName = userName, Id = appuser.Id });
Clients.Caller.onConnected(id, userName, Users);
Clients.AllExcept(id).onNewUserConnected(id, userName);
}
}
public override System.Threading.Tasks.Task OnDisconnected(bool stopCalled)
{
var item = Users.FirstOrDefault(x => x.ConnectionId == Context.ConnectionId);
if (item != null)
{
Users.Remove(item);
var id = Context.ConnectionId;
Clients.All.onUserDisconnected(id, item.UserName);
}
return base.OnDisconnected(stopCalled);
}
}
The article says:
By default, there will be an implementation that uses the user's IPrincipal.Identity.Name as the user name.
This means that for the mapping to work, you need to make user that users are logged in (authorized) when calling SignalR hub. The easiest way to ensure this is to add [Authorize] attribute to ChatHub and the controller that returns chat view.
If user is authorized, you can retrieve his username from current context by calling:
var username = Context.User.Identity.Name;
Then, the following should work (without the need to provide your own IUserIdProvider):
public void SendPM(string name, string privatemessage, username)
{
Clients.Client(username).addPM(name, privatemessage);
}
I am using MVC 5 and Asp Identity for a new app I am building. I need to modify the login process so only information pertaining to the users community is shown. I was thinking of creating another table with Community ID's and User ID's in it similar to the AspUserRoles table. How do I tie this into the login process? Sorry I am new to MVC coming from Web Forms :(
Thanks!
Seems like a valid approach. So you'll end up with something like:
public class Community
{
...
public virtual ICollection<ApplicationUser> Members { get; set; }
}
public class ApplicationUser : IdentityUser
{
...
public virtual ICollection<Community> Communities { get; set; }
}
Entity Framework will automatically generate a join table from that. Then, in your login, somehow, you feed the community id. That could be from a special URL, a hidden input, select box, whatever. It's up to you. Then, you just need to modify the sign in process slighly. By default in a generated project template, sign in is handled via the following line:
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
You can see the code for PasswordSignInAsync here. But I'll post it here for posterity as well:
public virtual async Task<SignInResult> PasswordSignInAsync(string userName, string password,
bool isPersistent, bool shouldLockout)
{
var user = await UserManager.FindByNameAsync(userName);
if (user == null)
{
return SignInResult.Failed;
}
return await PasswordSignInAsync(user, password, isPersistent, shouldLockout);
}
So, just add your own version of this to ApplicationSignInManager in IdentiyConfig.cs:
public virtual async Task<SignInResult> PasswordSignInAsync(int communityId, string userName, string password,
bool isPersistent, bool shouldLockout)
{
var user = await UserManager.FindByNameAsync(userName);
if (user == null || !user.Communities.Any(m => m.Id == communityId))
{
return SignInResult.Failed;
}
return await PasswordSignInAsync(user, password, isPersistent, shouldLockout);
}
Notice the extra condition. Then, you just need to modify the original call in the Login action to pass in the communityId:
var result = await SignInManager.PasswordSignInAsync(communityId, model.Email, model.Password, model.RememberMe, shouldLockout: false);