I found this resource for creating an AuthorizationHandler and followed along to create one. My handler checks for given_name, a string value, and if given_name has a value of "Bob", authorization success.
The problem, I can't access the given_name claim and its value. I can see all the expected claims when I inspect context.User but context.User.HasClaim(c => c.Type == ClaimTypes.GivenName) always returns false.
How do I check for the presence of a claim and get its value?
Update - As a workaround, I can access all the claims by calling .ToList() on context.User.Claims and then using .Any() on the list. It works but I haven't seen this approach in any examples.
For Asp.NET Core 6, you can pass in the AuthorizationHandlerContext.
Here is a sample AuthorizationHandler that leverages that context for a ClaimsPrincipal.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
namespace MyIdentityApp.Security
{
public class CanReadAffiliatedOwnerInfoHandler : AuthorizationHandler<ManageAffiliatedOwnerInfoRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ManageAffiliatedOwnerInfoRequirement requirement)
{
var authFilterContext = (HttpContext?)context.Resource;
if (authFilterContext == null)
{
return Task.CompletedTask;
}
else
{
//access user's claims here, via context.User
var loggedInUserAffiliatedOwners = context.User.Claims.FirstOrDefault(c => c.Value == OwnerIdAttemptingToAccess);
if (loggedInUserAffiliatedOwners != null)
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
}
Related
I am doing an Asp.Net Core Mvc 6 App where I dynamically assign Claims to the user.
I have in the database all Roles that a user must have to access specific fields of the page.
I also have the role of the user.
I am creating Claims where the user's role matches the field page User role. And then I create a ClaimsPrincipal with those list of Roles.
This are the data in the database
This is how I create the permission for the user..
private string[] TransformRowsIntoPermissions(List<RolesAccessModel>? rows, string userRole)
{
List<string> permissionList = new();
if(rows!=null)
{
foreach (RolesAccessModel row in rows)
{
if (row.Roles!=string.Empty && row.Roles != null && !row.Roles.Contains(userRole))
continue;
// if we get here we have a match
if (!permissionList.Contains(row.EventName))
permissionList.Add(row.EventName);
}
}
return permissionList.ToArray();
}
When I have the Permission List, I add it as Claims
private Claim[] CreateClaimsFromArray(string[] permissions)
{
List<Claim> claims = new();
foreach (var permission in permissions)
claims.Add(new Claim(permission, "-"));
return claims.ToArray();
}
And in the main function I save it as ClaimPrincipal
private async void CreateClaimsByUserRole(string role)
{
ClaimsIdentity claimsIdentity =await _iUIConfig.CreateClaimsByUserRole(role);
var userPrincipal = new ClaimsPrincipal(new[] { claimsIdentity });
_ = HttpContext.SignInAsync(userPrincipal);
}
I do not know if line _ = HttpContext.SignInAsync(userPrincipal); is necessary.
I checked userPrincipal and it has all the data.
What I need to do is to ask in the View for that claims in order to show or to not Show the fields.
But when I ask if it is null...
#{
var claimsIdentity = User.Identity as System.Security.Claims.ClaimsIdentity;
if (claimsIdentity != null)
{
var c = claimsIdentity.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier);
if (c != null)
{
<p>
#c.Type.ToString();
#c.Value.ToString();
</p>
}
else
{
<p>
null
</p>
}
}
}
It is null
If I ask for a specific value
#{
if(System.Security.Claims.ClaimsPrincipal.Current.Claims.ToList().FirstOrDefault(c => c.Type == "DocumentId" && c.Value == "-") != null)
{
.....
}
}
Got an error System.NullReferenceException: 'Object reference not set to an instance of an object.'
Even If a call a Controller/Method and ask for
var identity = (ClaimsPrincipal)Thread.CurrentPrincipal;
Is null..
I have to do it persistent for all the App.
I am not using Authentication because it is not implemented yet.. I am using SSO from another system. For that reason, for my understanding Cookie are used for Authentication.
In program.cs I only have
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminAccess", policy => policy.RequireRole("Admin"));
options.AddPolicy("ManagerAccess", policy =>
policy.RequireAssertion(context =>
context.User.IsInRole("Admin")
|| context.User.IsInRole("Manager")));
options.AddPolicy("UserAccess", policy =>
policy.RequireAssertion(context =>
context.User.IsInRole("Admin")
|| context.User.IsInRole("Manager")
|| context.User.IsInRole("User")));
});
and
app.UseAuthorization();
What am I missing to do it persistent?
Thanks
ClaimsPrincipal.Current was used in asp.net,however,in Asp.net core it is no longer set
To retrieve the current user in an ASP.NET Core MVC app,you could try with ControllerBase.User orHttpContext.User
You could check this document for more details
SignInAsync creates an encrypted cookie and adds it to the current response.You could get the claims after authenticating successfully in the following request untill the cookie expires(not the current request)
Notice HttpContext.User could be setted directly
And in View you could get the claims in # section as below:
#{
var claims = this.ViewContext.HttpContext.User.Claims.ToList();
}
In a regular type scenario, where a Route is available, say to only "Premium" users, ocelot.global.json would have RouteClaimsRequirement like this:
"RouteClaimsRequirement" : { "Role" : "Premium" }
This would get translated to a KeyValuePair<string, string>(), and it works nicely.
However, if I were to open a route to 2 types of users, eg. "Regular" and "Premium", how exactly could I achieve this?
I found a way through overriding of default Ocelot middleware. Here are some useful code snippets:
First, override the default AuthorizationMiddleware in Configuration() in Startup.cs:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
var config = new OcelotPipelineConfiguration
{
AuthorisationMiddleware
= async (downStreamContext, next) =>
await OcelotJwtMiddleware.CreateAuthorizationFilter(downStreamContext, next)
};
app.UseOcelot(config).Wait();
}
As you can see, I am using a custom OcelotJwtMiddleware class up there. Here is that class, pasted:
public static class OcelotJwtMiddleware
{
private static readonly string RoleSeparator = ",";
public static Func<DownstreamContext, Func<Task>, Task> CreateAuthorizationFilter
=> async (downStreamContext, next) =>
{
HttpContext httpContext = downStreamContext.HttpContext;
var token = httpContext.Request.Cookies[JwtManager.AuthorizationTokenKey];
if (token != null && AuthorizeIfValidToken(downStreamContext, token))
{
await next.Invoke();
}
else
{
downStreamContext.DownstreamResponse =
new DownstreamResponse(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}
};
private static bool AuthorizeIfValidToken(DownstreamContext downStreamContext, string jwtToken)
{
IIdentityProvider decodedObject = new JwtManager().Decode<UserToken>(jwtToken);
if (decodedObject != null)
{
return downStreamContext.DownstreamReRoute.RouteClaimsRequirement["Role"]
?.Split(RoleSeparator)
.FirstOrDefault(role => role.Trim() == decodedObject.GetRole()) != default;
}
return false;
}
}
JwtManager class here is just my small utility made using the default Jwt NuGet package, nothing special. Also, JWT is being stored as a Cookie, which is not safe, but doesn't matter here. If you happen to copy paste your code, you will have small errors relating to this, but just switch it out with your own implementations of auth tokens.
After these 2 snippets were implemented, ocelot.global.json can have RouteClaimsRequirement such as this:
"RouteClaimsRequirement" : { "Role" : "Premium, Regular" }
This will recognize both clients with Regular in their Cookies, as well as those with Premium.
I am using identity core for user management in .net core 3.1 web api. Now, I want to check the users email for something and if it meets the requirement only then he will be created. The code below tells a lot about what I want to achieve
I have a custom user validator as below:
public class CustomEmailValidator<TUser> : IUserValidator<TUser>
where TUser : User
{
public Task<IdentityResult> ValidateAsync(UserManager<TUser> manager,
TUser user)
{
User userFromEmail = null;
if(!string.IsNullOrEmpty(user.Email))
userFromEmail = manager.FindByEmailAsync(user.Email).Result;
if (userFromEmail == null)
return Task.FromResult(IdentityResult.Success);
return Task.FromResult(
IdentityResult.Failed(new IdentityError
{
Code = "Err",
Description = "You are already registered with us."
}));
}
}
I add the validator in my startup as below:
services.AddDbContext<DataContext>(x => x.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
IdentityBuilder builder = services.AddIdentityCore<User>(opt =>
{
opt.User.RequireUniqueEmail = false;
opt.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyz0123456789._-";
opt.Password.RequireDigit = true;
opt.Password.RequiredLength = 6;
opt.Password.RequireNonAlphanumeric = true;
opt.Password.RequireUppercase = true;
opt.Password.RequireLowercase = true;
})
.AddUserValidator<CustomEmailValidator<User>>();
builder = new IdentityBuilder(builder.UserType, typeof(Role), builder.Services);
builder.AddEntityFrameworkStores<DataContext>();
builder.AddRoleValidator<RoleValidator<Role>>();
builder.AddRoleManager<RoleManager<Role>>();
builder.AddSignInManager<SignInManager<User>>();
As can be seen, I want to use the default user validation and my custom validation too. The problem being the user gets created right after the default validation and the email always turns out as exists in my custom validation. I don't really want to override my default validations.
Creating the user as below:
[HttpPost("Register")]
public async Task<IActionResult> Register(UserForRegisterDto userForRegister)
{
var userToCreate = _mapper.Map<User>(userForRegister);
var result = await _userManager.CreateAsync(userToCreate, userForRegister.Password);
if (result.Succeeded)
{
var roleresult = await _userManager.AddToRoleAsync(userToCreate, "Member");
return Ok(roleresult);
}
return BadRequest(result.Errors);
}
Note This is not my actual use case. I know I can check for unique email in my default validation by making opt.User.RequireUniqueEmail = true. This is just to clear a concept for further development.
Update After further debugging, I see that the custom validation method is called twice. Once before user creation and once after creation for some reason. I insert a new unique email and the custom validation passes success and after user creation, custom validation is called again and find the email registered already and throws an error message. This is weird
Found out that AddToRoleAsync was calling the custom validator again and was finding the user present in the table. Had to include a check whether the user found in the table with the same email is the same as user getting getting updated.
Code below:
public class CustomEmailValidator<TUser> : IUserValidator<TUser>
where TUser : User
{
public Task<IdentityResult> ValidateAsync(UserManager<TUser> manager,
TUser user)
{
User userFromEmail = null;
if(!string.IsNullOrEmpty(user.Email))
userFromEmail = manager.FindByEmailAsync(user.Email).Result;
if (userFromEmail == null)
return Task.FromResult(IdentityResult.Success);
else {
if(string.Equals(userFromEmail.Id, user.Id))
{
return Task.FromResult(IdentityResult.Success);
}
}
return Task.FromResult(
IdentityResult.Failed(new IdentityError
{
Code = "Err",
Description = "You are already registered with us."
}));
}
}
This should help a lot of people
I am working on .net core project. I am trying to implement authorize using AD groups. My requirement is, I have many groups in the azure ad. If the current user belongs to any of the available groups in azure ad then I want to authorize those users to access apis written in .net core application. I tried as below. I have added below two classes
public class IsMemberOfGroupHandler : AuthorizationHandler<IsMemberOfGroupRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context, IsMemberOfGroupRequirement requirement)
{
var groupClaim = context.User.Claims
.FirstOrDefault(claim => claim.Type == "groups" &&
claim.Value.Equals(requirement.GroupId, StringComparison.InvariantCultureIgnoreCase));
if (groupClaim != null)
context.Succeed(requirement);
return Task.CompletedTask;
}
}
public class IsMemberOfGroupRequirement : IAuthorizationRequirement
{
public readonly string GroupId;
public readonly string GroupName;
public IsMemberOfGroupRequirement(string groupName, string groupId)
{
GroupName = groupName;
GroupId = groupId;
}
}
Below is my startup class.
services.AddAuthorization(options =>
{
var adGroupConfig = new List<AdGroupConfig>();
_configuration.Bind("AdGroups", adGroupConfig);
foreach (var adGroup in adGroupConfig)
options.AddPolicy(
adGroup.GroupName,
policy =>
policy.AddRequirements(new IsMemberOfGroupRequirement(adGroup.GroupName, adGroup.GroupId)));
});
Above code checks groups available in configuration file. Now my requirement is use microsoft graph api to get all the available groups. I could not find any way to handle this requirement. Can someone help me with this? Any help would be appreciated. Thanks
Please firstly check this code sample , which use OpenID Connect to sign in users and use MSAL to get the Microsoft Graph API token to retire groups .
If config the your application to receive group claims by editing the manifest :
{
...
"errorUrl": null,
"groupMembershipClaims": "SecurityGroup",
...
}
The object id of the security groups the signed in user is member of is returned in the groups claim of the token.
If a user is member of more groups than the overage limit (150 for SAML tokens, 200 for JWT tokens), then the Microsoft Identity Platform does not emit the groups claim in the token. Instead, it includes an overage claim in the token that indicates to the application to query the Graph API to retrieve the user’s group membership.
{
...
"_claim_names": {
"groups": "src1"
},
{
"_claim_sources": {
"src1": {
"endpoint":"[Graph Url to get this user's group membership from]"
}
}
...
}
So you can follow the process :
Check for the claim _claim_names with one of the values being groups. This indicates overage.
If found, make a call to the endpoint specified in _claim_sources to fetch user’s groups.
If none found, look into the groups claim for user’s groups.
Of course , you can directly call Microsoft Graph API to retire current user's groups without using group claims :
https://learn.microsoft.com/en-us/graph/api/user-list-memberof?view=graph-rest-1.0&tabs=http
You can then authorize based on that groups . For example , if using policy :
services.AddAuthorization(options =>
{
options.AddPolicy("GroupsCheck", policy =>
policy.Requirements.Add(new GroupsCheckRequirement("YourGroupID")));
});
services.AddScoped<IAuthorizationHandler, GroupsCheckHandler>();
GroupsCheckRequirement.cs:
public class GroupsCheckRequirement : IAuthorizationRequirement
{
public string groups;
public GroupsCheckRequirement(string groups)
{
this.groups = groups;
}
}
GroupsCheckHandler.cs :
public class GroupsCheckHandler : AuthorizationHandler<GroupsCheckRequirement>
{
private readonly ITokenAcquisition tokenAcquisition;
private readonly IMSGraphService graphService;
public GroupsCheckHandler(ITokenAcquisition tokenAcquisition, IMSGraphService MSGraphService)
{
this.tokenAcquisition = tokenAcquisition;
this.graphService = MSGraphService;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
GroupsCheckRequirement requirement)
{
string accessToken = await tokenAcquisition.GetAccessTokenOnBehalfOfUserAsync(new[] { Constants.ScopeUserRead, Constants.ScopeDirectoryReadAll });
User me = await graphService.GetMeAsync(accessToken);
IList<Group> groups = await graphService.GetMyMemberOfGroupsAsync(accessToken);
var result = false;
foreach (var group in groups)
{
if (requirement.groups.Equals(group.Id))
{
result = true;
}
}
if (result)
{
context.Succeed(requirement);
}
}
}
And then using policy :
[Authorize(Policy = "GroupsCheck")]
You can use this graph api to get all the groups the user is a direct member of.
GET /me/memberOf
In .net-core you can use GraphServiceClient to call graph api. Here is a sample for your reference.
var graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(
(requestMessage) =>
{
// Get back the access token.
var accessToken = "";
if (!String.IsNullOrEmpty(accessToken))
{
// Configure the HTTP bearer Authorization Header
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
}
else
{
throw new Exception("Invalid authorization context");
}
return (Task.FromResult(0));
}
));
var groups = graphClient.Me.MemberOf.Request().GetAsync().Result;
I'm converting my asp.net framework to asp.net core.
One thing I'm facing with is saving query data in Authentication context in authorizationhandler.
In my asp.net framework, I've done with my AuthorizeAttribute in ASP.Net Framework:
public override void OnAuthorization(HttpActionContext actionContext)
{
// Retrieve email and password.
var accountEmail =
actionContext.Request.Headers.Where(
x =>
!string.IsNullOrEmpty(x.Key) &&
x.Key.Equals(HeaderFields.RequestAccountEmail))
.Select(x => x.Value.FirstOrDefault())
.FirstOrDefault();
// Retrieve account password.
var accountPassword =
actionContext.Request.Headers.Where(
x =>
!string.IsNullOrEmpty(x.Key) &&
x.Key.Equals(HeaderFields.RequestAccountPassword))
.Select(x => x.Value.FirstOrDefault()).FirstOrDefault();
// Invalid account name or password.
if (string.IsNullOrEmpty(accountEmail) || string.IsNullOrEmpty(accountPassword))
{
// Treat this request is unauthorized.
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized, new
{
Error = $"{Language.WarnAccountNotLogin}"
});
return;
}
// Find the hashed password from the original one.
var accountHashedPassword = RepositoryAccountExtended.FindMd5Password(accountPassword);
// Retrieve person whose properties match conditions.
var person = RepositoryAccountExtended.FindPerson(null, accountEmail, accountHashedPassword, null, null);
// No person has been found.
if (person == null)
{
// Treat this request is unauthorized.
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized, new
{
Error = $"{Language.WarnAccountNotLogin}"
});
return;
}
// Account has been disabled.
if ((StatusAccount) person.Status == StatusAccount.Inactive)
{
// Treat the login isn't successful because of disabled account.
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized, new
{
Error = $"{Language.WarnDisabledAccount}"
});
return;
}
// Account is still pending.
if ((StatusAccount) person.Status == StatusAccount.Pending)
{
// Treat the login isn't successful because of pending account.
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized, new
{
Error = $"{Language.WarnPendingAccount}"
});
return;
}
// Account role isn't enough to access the function.
if (!Roles.Any(x => x == person.Role))
{
// Role isn't valid. Tell the client the access is forbidden.
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Forbidden, new
{
Error = $"{Language.WarnForbiddenAccessMethod}"
});
}
// Store the requester information in action argument.
actionContext.ActionArguments[HeaderFields.Account] = person;
}
As you see, I stored my query data (Account - in this situation) in the actionContext, and I can access to it later in Controllers.
My question is: How can I achieve the same thing in ASP.NET Core, because I don't want to query my database in my every AuthorizationHandler.
Thank you,
How can I achieve the same thing in ASP.NET Core
First you need an authentication middleware, for your case it may be basic authentication. For Aspnet Core there is no built-in basic authentication middleware. A soluton is here or you can implement own authentication middleware like this.
I stored my query data (Account - in this situation) in the
actionContext, and I can access to it later in Controllers.
Two possible ways are coming to my mind:
Adding parameter into HttpContext.Items
Adding claim to current User.Identity
To implement this you can use ClaimsTransformation or custom middleware after authentication middleware. If you go with your own implementation you can also use HandleAuthenticateAsync method.
Update
It seems right place to save query data is HandleAuthenticateAsync. If you use #blowdart's basic authentication solution, your code might be something like below:
.....
await Options.Events.ValidateCredentials(validateCredentialsContext);
if (validateCredentialsContext.Ticket != null)
{
HttpContext.Items[HeaderFields.Account] = person; // assuming you retrive person before this
Logger.LogInformation($"Credentials validated for {username}");
return AuthenticateResult.Success(validateCredentialsContext.Ticket);
}