How to customize hangfire dashboard only show authenticated user's owned jobs? - hangfire

I am using hangfire and Hangfire.Dashboard.Authorization to configure access to hangfire dashboard:
public void Configure(IAppBuilder app)
{
var options = new DashboardOptions
{
AuthorizationFilters = new []
{
new AuthorizationFilter { Users = "admin"},
new ClaimsBasedAuthorizationFilter("name", "value")
}
};
app.UseHangfireDashboard("/myjobs", options);
}
I would like to provide access to all authenticated users to dasboard but only allow view jobs what belongs to the current user (started by she/he). Is it possible somehow filter the dashboard access and results by the current user?

Related

Blazor WASM AuthenticationState using AAD - Claims null until Refresh

New to Blazor and have been doing a hatchet job to get things working how I want.
I am using Blazor WASM with AAD for Authentication created based on this document MS Doc. I implemented the SecureAccountFactory class from the example and call a db where I get the associated user based on the AAD Guid, then add everything into Claims.
public async override ValueTask<ClaimsPrincipal> CreateUserAsync(SecureUserAccount account,
RemoteAuthenticationUserOptions options)
{
var initialUser = await base.CreateUserAsync(account, options);
if (initialUser.Identity.IsAuthenticated)
{
var userIdentity = (ClaimsIdentity)initialUser.Identity;
var claims = userIdentity.Claims;
var principalId = claims.Where(x => x.Type == "oid").First();
//Get some user info from SQL
var User = await _UserService.Get(principalId.Value);
//Get user Roles from SQL and add to Claims
var UsersInRoles = await _UsersInRoleService.RolesByUserId(principalId.Value);
//Add the ClientId to Claims
userIdentity.AddClaim(new Claim("clientId", User.ClientId.ToString()));
foreach (var userrole in UsersInRoles)
{
userIdentity.AddClaim(new Claim("appRole", userrole.Role.Name));
}
}
return initialUser;
}
I then have a Profile Component that appears on every page as part of the MainLayout which should have some info about the current user, so I made a static class to retrieve this info.
public static class UserHelper
{
public static async Task<CurrentUserClaims> GetCurrentUserClaims(Task<AuthenticationState> authenticationStateTask)
{
AuthenticationState authenticationState;
authenticationState = await authenticationStateTask;
var AuthenticationStateUser = authenticationState.User;
var user = authenticationState.User;
var claims = user.Claims;
var clientClaim = claims.Where(x => x.Type == "clientId").First();
var principalId = claims.Where(x => x.Type == "oid").First();
return new CurrentUserClaims
{
ClientId = Convert.ToInt32(clientClaim.Value),
PrincipalId = Guid.Parse(principalId.Value),
user = user
};
}
}
In my ProfileComponent, I call CascadingParameter and then onParametersSet I query my Static class for the info from the current logged in user
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
private string profilePath;
protected override async Task OnParametersSetAsync()
{
CurrentUserClaims UserClaims = await UserHelper.GetCurrentUserClaims(authenticationStateTask);
var principal = UserClaims.PrincipalId;
//... do stuff
}
The above all works, after a Refresh or once I route to any other page. The initial Load, after login on the home page shows that the below line always fails with 'Sequence contains no elements'
var clientClaim = claims.Where(x => x.Type == "clientId").First();
I am using Authorize to protect the pages and I will eventually be using the Roles to determine what to display to the user.
A: Surely there's a better way of doing the above. There are lots and lots of articles on creating a custom Auth which inherits AuthenticationState but every one I've seen adds the Claims manually as a fake user, so I don't see how to access the actual Claims.
B: I'm wondering if just using LocalStorage for the User info might be a simpler way to go but is it considered 'safe' or best practice?
Any pointers to a solution are appreciated.

Request a MS Graph Access token from Middleware

I'm trying to request MS Graph API from an API Middleware, in ASP.Net, to create programmatically Office Planners on specific api calls.
Customers are logged in to the frontend application via Azure AD SSO (using adal).
This access token allow users to authenticate to my api.
Now, i want to request a MS Graph access token from this API access token, to create the planners from the authenticated user account (on behalf of).
How can i proceed ?
Could the OBO flow (On-Behalf-Of flow) work in this case ?
What Tiny Wang has said is correct, you could do that in Program.cs then use dependency injection for the Graph client inside your controller.
Here's a quick example below of posting an event to the authenticated users calendar.
namespace MyApp.Controllers;
[Authorize][AuthorizeForScopes(ScopeKeySection = "DownstreamApi:Scopes")]
public class HomeController: Controller {
private readonly GraphServiceClient _graphClient;
public HomeController(GraphServiceClient graphClient) {
_graphClient = graphClient;
}
public async Task AddUserCalendarEvent() {
var #event = new Event {
Subject = "Testing",
Body = new ItemBody {
ContentType = BodyType.Html,
Content = "Test Body"
},
Start = new DateTimeTimeZone {
DateTime = "2022-02-18T12:00:00",
TimeZone = "Europe/London"
},
End = new DateTimeTimeZone {
DateTime = "2022-02-18T12:00:00",
TimeZone = "Europe/London"
}
};
await _graphClient.Me.Events.Request().AddAsync(#event);
}
}

How to use Microsoft Graph API to get all the groups for Authorization in .net core application?

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;

Control users allowed to login to clients with SSO identityServer4

IdentityServer4 provides SSO experience along with the STS functionalities. SSO works As soon as the clients are validated as per the OAuth OpenId Connect. So the users if the identity is based on the cookie.
I was wondering if any one implemented this for multitenancy fashion to restrict users to allow only set of clients (means SSO still works among them - let's call the group as a tenant). When it comes to non-allowed users and clients, the identity server should take them to a login screen.
Configuration on the startup - AspNetIdentity for users store
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddUserManager<CustomUserManager>()
.AddSignInManager<CustomSignInManager>()
.AddDefaultTokenProviders();
//TODO Tenant based cookie SaasKit/Finbuckle
services.ConfigureApplicationCookie(cookieOptions =>
{
cookieOptions.Cookie = new Microsoft.AspNetCore.Http.CookieBuilder
{
Name="Tenant_Cookie"
};
});
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
}).AddSigningCredential(GetSigningCertificate("my", "a7 e2 f5 f7 9a b8 8c 86 2c 37 f5 22 1b ea 8c 19 b1 58 99 3c", true))
.AddResponseGenerators()
.AddCustomAuthorizeRequestValidator<TenantAuthorizeRequestValidator>()
.AddCustomTokenRequestValidator<TenantTokenRequestValidator>()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddProfileService<CustomProfileService>()
.AddAspNetIdentity<ApplicationUser>();
Tenant Token request validator - Validate user and client
public class TenantTokenRequestValidator : ICustomTokenRequestValidator
{
HttpContext _context;
IClientService _clientService;
public TenantTokenRequestValidator(IHttpContextAccessor contextAccessor, IClientService clientService)
{
_context = contextAccessor.HttpContext;
_clientService = clientService;
}
public Task ValidateAsync(CustomTokenRequestValidationContext context)
{
if (!context.Result.IsError)
{
//AuthorizationCode authorization_code
if (context.Result.ValidatedRequest.UserName!=null &&
!_clientService.IsValidUser(context.Result.ValidatedRequest.Client.ClientId,context.Result.ValidatedRequest.UserName))
{
context.Result.IsError = true;
context.Result.Error = "UnauthorizedUser";
}
}
return Task.CompletedTask;
}
}
Tenant authorize request validator - Validate Client and tenant
public class TenantAuthorizeRequestValidator : ICustomAuthorizeRequestValidator
{
HttpContext _context;
ITenantService _tenantService;
public TenantAuthorizeRequestValidator(IHttpContextAccessor contextAccessor,ITenantService tenantService)
{
_context = contextAccessor.HttpContext;
_tenantService = tenantService;
}
public Task ValidateAsync(CustomAuthorizeRequestValidationContext context)
{
if (!context.Result.IsError)
{
var tenant = context.Result.ValidatedRequest.GetTenant();
if (!string.IsNullOrEmpty(tenant))
{
if (!_tenantService.IsValidClient(tenant,context.Result.ValidatedRequest.ClientId))
{
context.Result.IsError = true;
context.Result.Error = OidcConstants.AuthorizeErrors.UnauthorizedClient;
}
context.Result.ValidatedRequest.ClientClaims.Add(new Claim(
TenantConstants.TokenKey,
tenant,
IdentityServerConstants.ClaimValueTypes.Json));
}
//Find a way to respond the error message
}
return Task.CompletedTask;
}
}
Database is like
public static Dictionary<string, string[]> TenantClients()
{
return new Dictionary<string, string[]>()
{
{ "tenant1",new string[]{ "tenant1.mvc","tenant1.mvc2" } },
{ "tenant2",new string[]{ "tenant2.mvc" } }
};
}
public static Dictionary<string, string[]> ClientUsers()
{
return new Dictionary<string, string[]>()
{
{ "tenant1.mvc", new string[]{"alice","bob"} },
{ "tenant1.mvc2", new string[]{"alice","bob"} },
{ "tenant2.mvc", new string[]{"alice"}}
};
}
I'm validating the client and tenant also client and user with the above. However not been able to achieve tenant based cookie so that different logins will work on the same browser session with different cookies. Saaskit doesnot seem to work well with aspnet core 2.0, Not finding a way with finbuckle.
question - How to set the cookie name with tenant? resolving from the request based on the context acr values. does this approach works?
We have end up adding an extra bunch of client specific claims to the user accounts and set the authorization policy based on the incoming claims.
For instance-
Api resource wants to restrict the access based on the scope in the token, which is fine for client validation, and for user validation looks for a specific claim in the token and gives access.
With the above approach we had to maintain the claims for users coming through UI, for other clients accessing server to server without user, into the client claims.
Though its not the best approach, this solved our current requirement.
The way we have done it is that you need to create several AddCookie in the startup one for each tenant with its own scheme name.
Then in the ICustomAuthorizeRequestValidator you set the scheme name to be the same as the scheme you created for the tenant (so probably you have to define a common format like: Cookies-MyTenantId1, or Cookies-MyTenantId2). This way a different cookie gets created for each tenant.

Using Roles with Forms Authentication

I'm using forms authentication in my MVC application. This is working fine. But not I want to adjust authorization to only allow people in certain roles. The logins correspond to users in active directory and the roles correspond to the groups the users are in.
For authentication, I simply call FormsAuthentication.SetAuthCookie(username, true) after verifying the login.
For authorizing, I first applied the attribute to the controllers I want to secure
[Authorize(Roles = "AllowedUsers")]
public class MyController
...
Next, I'm handling the OnAuthenticate event in global.asax.
protected void FormsAuthentication_OnAuthenticate(Object sender, FormsAuthenticationEventArgs args)
{
if (FormsAuthentication.CookiesSupported)
{
if (Request.Cookies[FormsAuthentication.FormsCookieName] != null)
{
try
{
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(
Request.Cookies[FormsAuthentication.FormsCookieName].Value);
// Create WindowsPrincipal from username. This adds active directory
// group memberships as roles to the user.
args.User = new WindowsPrincipal(new WindowsIdentity(ticket.Name));
FormsAuthentication.SetAuthCookie(ticket.Name, true);
}
catch (Exception e)
{
// Decrypt method failed.
}
}
}
else
{
throw new HttpException("Cookieless Forms Authentication is not " + "supported for this application.");
}
}
With this when someone accesses the website they get the login screen. From there they can actually log in. However, somehow it doesn't save the auth cookie and they get a login screen after the next link they click. I tried adding a call to SetAuthCookie() in OnAuthenticate() but they made no difference.
Before I added this event handler to handle authorization, authentication worked fine. So somewhere in the framework User is being set. I'm wondering if this the correct approach and I'm just missing something or if I need a different approach.
What do I need to do to get this to work?
Thanks,
Scott
It seems like my initial approach won't work. I was trying to get ASP.NET to automatically load user roles from their AD account. No comment was given on whether this was possible. However, the research I've done indicates I'll have to write code to load AD group memberships into user roles.
The solution to creating the user principal that ASP.NET MVC uses appears to be to create it in FormsAuthentication_OnAuthenticate() and assign it to Context.User. It appears if I don't set Context.User ASP.NET MVC creates a user principal based off the auth ticket after FormsAuthentication_OnAuthenticate() returns. Additionally, ASP.NET MVC appears to do nothing with Context.User if I set it in FormsAuthentication_OnAuthenticate().
The following is what I ended up doing.
This is the code that handles authentication
public ActionResult LogOn(FormCollection collection, string returnUrl)
{
// Code that authenticates user against active directory
if (authenticated)
{
var authTicket = new FormsAuthenticationTicket(username, true, 20);
string encryptedTicket = FormsAuthentication.Encrypt(authTicket);
var authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
authCookie.Expires = DateTime.Now.AddMinutes(30);
Response.Cookies.Add(authCookie);
if (Url.IsLocalUrl(returnUrl)
&& returnUrl.Length > 1
&& returnUrl.StartsWith("/", StringComparison.OrdinalIgnoreCase)
&& !returnUrl.StartsWith("//", StringComparison.OrdinalIgnoreCase)
&& !returnUrl.StartsWith("/\\", StringComparison.OrdinalIgnoreCase))
{
return Redirect(returnUrl);
}
else
{
return Redirect("~/");
}
}
return View();
}
I initially tried just calling FormsAuthentication.SetAuthCookie(username, true) instead of manually creating, encrypting, and adding it to the Response cookie collections. That worked in the development environment. However, it didn't after I published to the website.
This is the log off code
public ActionResult LogOff()
{
var authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
authCookie.Expires = DateTime.Today.AddDays(-1);
}
Response.Cookies.Add(authCookie);
FormsAuthentication.SignOut();
return RedirectToAction("Index", "Home");
}
FormsAuthentication.SignOut() doesn't seem to do anything after I switched to manually creating, encrypting, and adding the auth ticket to the response cookie collection in the logon code. So I had to manually expire the cookie.
This is the code I have for FormsAuthentication_OnAuthenticate()
protected void FormsAuthentication_OnAuthenticate(Object sender, FormsAuthenticationEventArgs args)
{
HttpCookie authCookie = Context.Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie == null || string.IsNullOrWhiteSpace(authCookie.Value))
return;
FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
UserData userData = null;
if (Application["UserData_" + authTicket.Name] == null)
{
userData = new UserData(authTicket.Name);
Application["UserData_" + authTicket.Name] = userData;
}
else
{
userData = (UserData)Application["UserData_" + authTicket.Name];
}
Context.User = new GenericPrincipal(new GenericIdentity(authTicket.Name), userData.Roles);
}
UserData is a class I created to handle caching user roles. This was needed because of the time it takes for active directory to return the group memberships the user belongs to. For completeness, the following is the code I have for UserData.
public class UserData
{
private int _TimeoutInMinutes;
private string[] _Roles = null;
public string UserName { get; private set; }
public DateTime Expires { get; private set; }
public bool Expired { get { return Expires < DateTime.Now; } }
public string[] Roles
{
get
{
if (Expired || _Roles == null)
{
_Roles = GetADContainingGroups(UserName).ToArray();
Expires = DateTime.Now.AddMinutes(_TimeoutInMinutes);
}
return _Roles;
}
}
public UserData(string userName, int timeoutInMinutes = 20)
{
UserName = userName;
_TimeoutInMinutes = timeoutInMinutes;
}
}
Roles can also be stored in a cookie and you have at least two options:
a role provider cookie (another cookie that supports the forms cookie), set with cacheRolesInCookie="true" on a role provider config in web.config. Roles are read the first time authorization module asks for roles and the cookie is issued then
a custom role provider that stores roles in the userdata section of the forms cookie, roles have to be added to the user data section of the forms cookie manually
The Authorization module asks the current principal for user roles, which, if role provider is enabled, either scans the role cookie (the first option) or fires the custom role provider methods.
Yet another, recommended approach is to switch to the Session Authentication Module (SAM) that can replace forms authentication. There are important pros, including the fact that SAM recreates ClaimsPrincipal out of the cookie and roles are just Role claims:
// create cookie
SessionAuthenticationModule sam =
(SessionAuthenticationModule)
this.Context.ApplicationInstance.Modules["SessionAuthenticationModule"];
ClaimsPrincipal principal =
new ClaimsPrincipal( new GenericPrincipal( new GenericIdentity( "username" ), null ) );
// create any userdata you want. by creating custom types of claims you can have
// an arbitrary number of your own types of custom data
principal.Identities[0].Claims.Add( new Claim( ClaimTypes.Role, "role1" ) );
principal.Identities[0].Claims.Add( new Claim( ClaimTypes.Role, "role2" ) );
var token =
sam.CreateSessionSecurityToken(
principal, null, DateTime.Now, DateTime.Now.AddMinutes( 20 ), false );
sam.WriteSessionTokenToCookie( token );
From now on, the identity is stored in a cookie and managed automatically and, yes, the Authorization attribute on your controllers works as expected.
Read more on replacing forms module with SAM on my blog:
http://www.wiktorzychla.com/2012/09/forms-authentication-revisited.html