I am new to webapi and mvc and I am struggling to find a best practice for handling authorizations dynamically based on roles and ownership of the resource. For example an account page that should allow employee admins, employee call center or the owning client to Get, Post, Put or Delete account information. So an admin and call center employee should be able to Get, Post, Put or Delete any request for any userid, but a client should only be able to perform these actions on resources owned by them.
For example Tom is UserID 10 and Jerry is UserID 20.
/api/Account/10 should be accessible by any admin, call center or Tom. Jerry should be kicked out.
/api/Account/20 should be accessible by any admin, call center or Jerry. Tom should be kicked out.
In webforms the typical solution is to just check if the user is a client and verify their id against the request. (I know AuthorizeAttribute is not in webforms, but showing as an example of what it would covert to in webapi/mvc.)
[Authorize(Roles = "Administrator, CallCenter, Client")]
public string Get(int userID)
{
if (Thread.CurrentPrincipal.IsInRole("Client") && Thread.CurrentPrincipal.Identity.userID != userID)
{
//Kick them out of here.
}
return "value";
}
This will work, but it seems like the check for ownership should happen in a single location before it reaches the controller and should be reusable throughout an application. I am guessing the best place would either be a custom AuthorizationFilterAttribute or a custom AuthorizeAttribute and maybe create a new role ClientOwner.
[Authorize(Roles = "Administrator, CallCenter, ClientOwner")]
public string Get(int userID)
{
return "value";
}
Custom AuthorizeAttribute
public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
{
//If user is already authenticated don't bother checking the header for credentials
if (Thread.CurrentPrincipal.Identity.IsAuthenticated) { return; }
var authHeader = actionContext.Request.Headers.Authorization;
if (authHeader != null)
{
if (authHeader.Scheme.Equals("basic", StringComparison.OrdinalIgnoreCase) &&
!String.IsNullOrWhiteSpace(authHeader.Parameter))
{
var credArray = GetCredentials(authHeader);
var userName = credArray[0];
var password = credArray[1];
//Add Authentication
if (true)
{
var currentPrincipal = new GenericPrincipal(new GenericIdentity(userName), null);
var user = GetUser(userName);
foreach (var claim in user.Cliams)
{
currentPrincipal.Identities.FirstOrDefault().AddClaim(new Claim(ClaimTypes.Role, claim);
}
//**************Not sure best way to get UserID below from url.***********************
if (user.userTypeID = UserTypeID.Client && user.userID == UserID)
{
currentPrincipal.Identities.FirstOrDefault().AddClaim(new Claim(ClaimTypes.Role, "ClientOwner"));
}
Thread.CurrentPrincipal = currentPrincipal;
return;
}
}
}
HandleUnauthorizedRequest(actionContext);
}}
Can someone point me in the right direction as to the best place to handle the authorization of the individual user? Should this still be done in the controller or should I move it to a custom AuthorizationFilterAttribute or a custom AuthorizationAttribute or is there somewhere else this should be handled? If the proper place is in a custom attribute, then what is the best way to get the userID and should I create a new role like the example above or should I do something different?
This is a common scenario and I am very surprised that I have struggled to find examples of the above scenario. This leads me to believe that either everyone is doing the check in the controller or there is another term I am not aware of so I am not getting good google results.
I think you may be getting authorization and permissions confused. "Dynamic authorization" isn't something you ever do.
Authorization is the act of verifying an author.
Request claims it is being sent from Alice.
Request presents a password or authorization token that proves the requester is Alice.
Server verifies that the password or authorization token matches its records for Alice.
Permissions are the business logic that specifies who can do what in your system.
Request is already authorized, and we know it came from Alice.
Alice is requesting to delete an important resource.
Is Alice an administrator? If not, tell her she can't do that because she doesn't have permission. (403 Forbidden)
The built-in [Authorize] attribute lets you optionally specify Roles that are permitted to access a resource. That option to specify permissions as part of authorization is slightly misplaced, in my opinion.
My advice would be to leave authorization as purely the process of verifying the author of a request. The BasicAuthHttpModule described here is close to what you want already.
Non-trivial permissions logic needs to be handled inside of your action body. Here's an example:
//Some authorization logic:
// Only let a request enter this action if the author of
// the request has been verified
[Authorize]
[HttpDelete]
[Route("resource/{id}")]
public IHttpActionResult Delete(Guid id)
{
var resourceOwner = GetResourceOwner(id);
//Some permissions logic:
// Only allow deletion of the resource if the
// user is both an admin and the owner.
if (!User.IsInRole("admin") || User.Identity.Name != resourceOwner)
{
return StatusCode(HttpStatusCode.Forbidden);
}
DeleteResource(id);
return StatusCode(HttpStatusCode.NoContent);
}
In this example, it would be difficult to convey the permissions logic as an attribute on the action, because the portion of the permissions that compares the current user to the resource owner can only be evaluated after you have actually gotten the resource owner info from your backend storage device.
Related
since I am rather new to SignalR and couldn't really find anything online, I want to ask my questions here.
I use SignalR with C# and .Net-Framework and want to Implement a function in which i can login with Username and Password to the Host/Hubs with specific Roles, but I couldn't really find anything helpfull in this regard at the Microsoft Docs.
(https://learn.microsoft.com/en-us/aspnet/signalr/overview/security/hub-authorization)
So my Question is:
How can I implement Authentication on my SignalR-Host and is it possible to reject a connection if a the Username and Password which is send to the Host isn't correct?
Thanks for the Help
DerDane
I use an Attribute for that:
public class AuthorizeRolesAttribute : AuthorizeAttribute
{
public AuthorizeRolesAttribute(params string[] roles)
{
this.Roles = string.Join(",", roles);
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
// Make sure the user is authenticated.
var roles = this.Roles.Split(new [] { ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var role in roles)
{
if (httpContext.User.IsInRole(role))
{
return true;
}
}
return false;
}
}
Usage:
[AuthorizeRoles("Admin")]
public class ExampleHub...
UPDATE
Authentication is not a trivial topic. If you want to dive deeper into it, take a look at https://learn.microsoft.com/en-us/aspnet/core/security/authentication/?view=aspnetcore-6.0 to understand how to authenticate a user. Then, at https://learn.microsoft.com/en-us/aspnet/core/security/authorization/roles?view=aspnetcore-6.0 you can see how roles are associated with a previously authenticated identity.
Generally, you will never do this manually. You use existing frameworks, which are already proven.
In httpContext.User.Identity you have access to your identity, the logged in user.
I think it can be very interesting for you to understand the pipeline of ASP .NET. Search for "asp net pipeline diagram" and also check this
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-6.0 for more information about the middleware.
There is a lot going on behind the scenes. You can just use them but it's always interesting to know them to a greater or lesser extent.
You define the roles. If your application is to manage a restaurant, you can create roles such as "waiter", "cook"... while in an educational center they can be "teacher", "student", "director", etc.
You can get a list of roles like this:
var roleStore = new RoleStore<IdentityRole>(context);
var roleMngr = new RoleManager<IdentityRole>(roleStore);
var roles = roleMngr.Roles.ToList();
To add a role to user, you can use UserManager.AddToRole.
I have front app on angular 5 and backend api on c# using identity server.
The problem is that when I click logout button, the token is removed and i am redirected to logout page.
But when I try to refresh main page, I am redirected to microsoftonline.com
authenticated automatically and redirected back to main page
I am missing providing username and password here, and this occurs in chrome incognito.
What I noticed is that if I remove manually the cookie from microsoftonline.com
and repeat the process, this time I will be asked for username and password.
So first I tried to clean all cookies this way but it din't help
foreach (var key in HttpContext.Request.Cookies.Keys)
{
HttpContext.Response.Cookies.Append(key, "", new CookieOptions() { Expires = DateTime.Now.AddDays(-1) });
}
bellow is my accountcontroller logout method and cookie screen
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutViewModel model)
{
var idp = User?.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
var subjectId = HttpContext.User.Identity.GetSubjectId();
if (idp != null && idp != IdentityServerConstants.LocalIdentityProvider)
{
if (model.LogoutId == null)
{
model.LogoutId = await interaction.CreateLogoutContextAsync();
}
try
{
await signInManager.SignOutAsync();
}
catch (NotSupportedException)
{
}
}
// set this so UI rendering sees an anonymous user
HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
// get context information (client name, post logout redirect URI and iframe for federated signout)
var logout = await interaction.GetLogoutContextAsync(model.LogoutId);
var vm = new LoggedOutViewModel
{
PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,
ClientName = logout?.ClientId,
SignOutIframeUrl = logout?.SignOutIFrameUrl
};
await persistedGrantService.RemoveAllGrantsAsync(subjectId, "angular2client");
return View("LoggedOut", vm);
}
If I understand correctly you are federating to Microsoft from your IdentityServer4 service? If so when you sign out of your identity service you should also give the user the option to sign out of the external provider (if it supports the relevant feature - it'd need to define an end_session_endpoint in the discovery document).
This functionality is supported by the standard OIDC middleware so you should be able to initiate signout by calling SignoutAsync() and passing the name of the scheme for the MS federated sign in.
Another option is to always send prompt=login in your external sign in requests and then check the auth_time claim you get back. That way to you force interactive sign in always and also verify when it happened.
Try cleaning the cookies from the HttpContext itself, using the extension method, provided by Identity Server, like here.
Or try this:
await HttpContext.SignOutAsync(IdentityServerConstants.DefaultCookieAuthenticationScheme);
in your Logout controller method.
3rd option (what I have in one of my test MVC clients is):
public ActionResult Logout()
{
Request.GetOwinContext().Authentication.SignOut();
return Redirect("/");
}
public void SignoutCleanup(string sid)
{
var cp = (ClaimsPrincipal)User;
var sidClaim = cp.FindFirst("sid");
if (sidClaim != null && sidClaim.Value == sid)
{
Request.GetOwinContext().Authentication.SignOut("Cookies");
}
}
Where the Logout method is called on the button click, and the SignoutCleanup is the one that is passed to Identity Server, when registering the client as a Client.BackChannelLogoutUri (or Client.FrontChannelLogoutUri, or both, depending on your scenario).
PS: Now, in general I think that your approach is not right, but I don't know your full case, so I'm not judging you - just giving and advice.
For front-end clients (Angular, Vue, vanilla JS etc..) it is recommended to use the client-side oidc-client-js library. And here is the usage example. As I said - this is just an advice, but if you are in the very beginning of your authentication setup, I would recommend you to have a look.
Sadly documentation on the implementation of a custom AuthorizeInteractionResponseGenerator in IdentityServer4 is sorely lacking.
I'm trying to implement my own AuthorizeInteractionResponseGenerator because I need a further step of user interaction (after authentication). My scenario is that a single identity (email) can be associated with multiple tenants. So after logon, I need the user to be presented with a list of associated tenants, so that they can choose one.
I have evaluated the source code, and have come up with the the following custom AuthorizeInteractionResponseGenerator:
public class AccountChooserResponseGenerator : AuthorizeInteractionResponseGenerator
{
public AccountChooserResponseGenerator(ISystemClock clock,
ILogger<AuthorizeInteractionResponseGenerator> logger,
IConsentService consent, IProfileService profile)
: base(clock, logger, consent, profile)
{
}
public override async Task<InteractionResponse> ProcessInteractionAsync(ValidatedAuthorizeRequest request, ConsentResponse consent = null)
{
var response = await base.ProcessInteractionAsync(request, consent);
if (response.IsConsent || response.IsLogin || response.IsError)
return response;
return new InteractionResponse
{
RedirectUrl = "/Organization"
};
}
}
It inherits from the base AuthorizeInteractionResponseGenerator built into IdentityServer4, so that the standard Logon and Consent pages can show. This happens, and then the user is correctly redirected to the /Organization url to select an organization (tenant).
But what then? With the lack of documentation and examples, I'm really struggling to figure out the following two questions:
1) How do I now, having selected a Tenant, indicate to my custom AccountChooserResponseGenerator that my interaction is complete, and that the user can now be redirected back to the Client?
Edit:
Answer to 1: To indicate that the interaction is complete, you I have to return an empty new InteractionResponse(). In my case, a check for the existence of the TenantId claim sufficed, as follows:
if (!request.Subject.HasClaim(c=> c.Type == "TenantId" && c.Value != "0"))
return new InteractionResponse
{
RedirectUrl = "/Organization"
};
return new InteractionResponse();
2) And how can I get information about the selected Tenant to be added to the identity token that IdentityServer4 passes back to the Client?
Edit: Answer to 2: In the Controller Action method that gets executed after selecting a Tenant, I called :
await HttpContext.SignInAsync(User.Claims.Single(r=> r.Type == "sub").Value,
new System.Security.Claims.Claim("TenantId", tenant.Id.ToString()));
return Redirect(ReturnUrl);
...which is an IdentityServer4-provided Extension to HttpContext.
In our developing e-commerce solution we are using AspNet Identity 2.2.1 and it is required that any guest (anonymous) users should complete checkout without prior registration to the website. In order to fullfill this requirement have written an ActionFilter named UserMigrationAttribute which obtains SessionTrackId (string GUID) from cookie -which we set from a HttpModule for every request if SessionTrackId is not found along with request cookies- and creates and actual IdentityUser in database with the username something like SessionTrackId#mydomain.com.
We have decorated our BaseController class with this UserMigration attribute in order to utilize its functions throughout the site.
Everything up to this point works as expected with single downside issue, which is when the page is being loaded for the first time for any user, if we try to make an Jquery Ajax Call to a Method which have [ValidateAntiForgeryToken] attribute, the call fails with the 'The provided anti-forgery token was meant for a different claims-based user than the current user.' error, even though we are sending __RequestVerificationToken parameter with every ajax call.
But if user opens another page by clicking link and/or reloads/refreshes current page, all the subsequent ajax calls complete successfully.
In our understanding UserMigrationAttribute creates user on OnActionExecuting method, but after we signIn user in the process #Html.AntiForgeryToken() is not being updated with the right values.
You may find the UserMigrationAttribute code below;
[AttributeUsage(AttributeTargets.Class)]
public class UserMigrationAttribute : ActionFilterAttribute
{
public ApplicationSignInManager SignInManager(ActionExecutingContext filterContext)
{
return filterContext.HttpContext.GetOwinContext().Get<ApplicationSignInManager>();
}
public UserManager UserManager(ActionExecutingContext filterContext)
{
return filterContext.HttpContext.GetOwinContext().GetUserManager<UserManager>();
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
CreateMigrateCurrentUser(filterContext);
base.OnActionExecuting(filterContext);
}
private static readonly object LockThis = new object();
private void CreateMigrateCurrentUser(ActionExecutingContext filterContext)
{
lock (LockThis)
{
var signInManager = SignInManager(filterContext);
var userManager = UserManager(filterContext);
var sessionTrackId = GetSessionTrackId(filterContext);
if (!filterContext.HttpContext.Request.IsAuthenticated)
{
if (!string.IsNullOrEmpty(sessionTrackId))
{
var username = string.Format("{0}#mydomain.com", sessionTrackId);
var user = userManager.FindByName(username);
if (user == null)
{
user = new User() {UserName = username, Email = username};
var result = userManager.Create(user);
userManager.AddToRole(user.Id, StringResources.AnonymousVisitorsGroup);
}
signInManager.SignIn(user, true, true);
}
}
else
{
if (!string.IsNullOrEmpty(sessionTrackId))
{
var username = string.Format("{0}#mydomain.com", sessionTrackId);
var user = userManager.FindByName(username);
if (user != null)
{
if (!HttpContext.Current.User.IsInRole(StringResources.AnonymousVisitorsGroup))
{
var targetUserId = HttpContext.Current.User.Identity.GetUserId<int>();
var service = new Service();
service.Users.MigrateUser(user.Id, targetUserId);
}
}
}
}
}
}
private string GetSessionTrackId(ActionExecutingContext filterContext)
{
var retVal = string.Empty;
if (filterContext.HttpContext.Request.Cookies["stid"] != null)
{
retVal = filterContext.HttpContext.Request.Cookies["stid"].Value;
}
return retVal;
}
}
Any help or suggestions are highly appreciated.
Thank you,
This is happening because the anti-forgery token is set in a cookie, which will not be updated until the next request. If you're manually signing a user in, you should also issue a redirect (even if to the same page they were already headed to), simply to ensure that the cookie data is correct. This normally happens naturally, as the sign in form will redirect to the URL that needed authorization after the user is signed in, thus negating the problem. Since you're not redirecting currently, the data is out of sync.
However, I have to say that this seems like a very poor solution to this particular use case. Creating some sort of temporary-type user and signing that user in to handle guest checkout creates an unnecessary glut of useless data in your database, at best, and leads to bugs and other issues like this one you're experiencing, at worst.
I also run an ecommerce site, and the way we handled guest checkout is incredibly simplistic. The checkout data is just stored in the session (email, shipping/billing address, etc.). We build a view model to handle the actual checkout where the data necessary for submitting the sale comes either from the user object, if they're logged in, or these session variables, if they aren't. If the user is neither logged in, nor has the requisite session variables set, then they are redirected to the onboarding form where billing/shipping, etc. is collected.
For other aspects like maintaining an anonymous cart, we use a permanent cookie with the cart identifier. If the user ends up creating an account, we associate the anonymous cart with their user, and then remove the cookie. This ensures that their cart survives past the session timeout and things like closing the browser, even if they're anonymous.
In other words, in all these things, no user object is actually needed. If it's there (user is logged in), great, we'll use it. Otherwise, we collect and persist the requisite information for checkout via other means.
I have made my API's secure using Authorize attribute.
If the request does not have valid OAuth token, it does not process and returns authorization error.
In my User table I have added 'Status' column to check if the user is Active(Soft delete) or not.
I want to use that 'Status' value(Of User Entity) to authenticate my API's.
Suppose Status is false, I want framework should not process the request as like in case 'Authorize attribute'.
Can it be possible ? If so then How ?
Note:
Now I am checking 'Status' value in my Action and decide to proceed or
not.
If you have other best alternatives then those are also welcome.
Thanks.
You could implement your own AuthorizeAttribute
Here's an example I used to implement my own Authorize method based on Roles:
public class AuthorizeRoles : AuthorizeAttribute
{
private readonly List<string> _rolesAllowed;
public AuthorizeRoles(string rolesAllowed)
{
this._rolesAllowed = rolesAllowed.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
}
public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
{
var controller = actionContext.ControllerContext.Controller;
if (!this._rolesAllowed.Contains("*"))
{
string errorMessage = "Authorization denied. Missing required role";
actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
}
}
else
{
string errorMessage = "Authorization denied. Request is missing context header";
actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
}
base.OnAuthorization(actionContext);
}
you then use it in your api methods like this :
[AuthorizeRoles("some value")] or [AuthorizeRoles("*")]
You could effectively specify which methods allow access to people with a certain value in their status.
You have access to the controller, you can check any values you need and allow or deny access based on the results of your check.
I have removed some of my less interesting code but what's left should give you an indication of how to proceed.
How I would do it is I would create a Roles table with different access levels. On my controller I would use the Roles feature of the Authorize attribute to allow or block users from accessing the Action.
[Authorize(Roles = "isActive")]