Background
I've created a working bot in C# but I'm failing to expand it to be a multi-tenant bot. I have created multiple bots in the Microsoft portal using this technique to identify themselves from the messaging endpoint:
https://example.com/api/messages/bot1
https://example.com/api/messages/bot2
https://example.com/api/messages/bot3
I can grab the LastSegment from the URL while in the MessagesController and store it in PrivateConversationData so I know which bot is talking in the current conversation. I intended use this stored 'bot id' in order to retrieve the Microsoft AppId & Password from the web.config (the bot's credentials are stored as a series of custom entries and not the standard appSettings as that only works for a single bot).
Credentials Problem
The authentication works well (nearly) as described here except when using async code with .ConfigureAwait(false) I can't get the HttpContext.Current as it becomes null when running on a different thread. This means I can't get the authenticated user's credentials either by looking them up in the web.config or by calling GetCredentialsFromClaims() since I've lost the authenticated user. If I use .ConfigureAwait(true) I just get deadlocks all over the place.
I have the credentials in the web.config but they are stored per bot and I need the 'bot id' from the URL above in order to get the credentials.
Question
The crux of the problem is: I need the URL to get the 'bot id' and I need the 'bot id' to get the credentials from the web.config but I can never reliably get access to the URL once I've passed a .ConfigureAwait(false) in the code. On the flip side, I can't get the 'bot id' from the PrivateConversationData since I need the bot's credentials in order to load it. A bit chicken and egg :-(
If anyone has any ideas of what I may be doing wrong or has an alternative approach to know which 'bot id' is currently executing I'd very much appreciate it.
Thanks
Please find below given the sample code.
public class StartUp {
public void Configuration(IAppBuilder app) {
var builder = new ContainerBuilder();
//Note: Initialize / register the Metadata Service that can bring the tenant details from the corresponding store
builder.RegisterType<TenantMetadataService>().As<ITenantMetadataService>();
//Note: This helps you in accessing the TenantMetadata from any constructor going forward after the below registry
builder.Register(ti => TenantMetadata.GetTenantMetadataFromRequest()).InstancePerRequest();
//TODO: Register the various services / controllers etc which may require the tenant details here
}
}
public class TenantMetadata {
public Guid TenantId { get;set; }
public Uri TenantUrl { get;set; }
public string TenantName { get;set; }
public static TenantMetadata GetTenantMetadataFromRequest() {
var context = HttpContext.Current;
//TODO: If you have any header like TenantId coming from the request, you can read and use it
var tenantIdFromRequestHeader = "";
//TODO: There will be a lazy cache that keeps building the data as new tenant's login or use the application
if(TenantCache.Contains(...))return TenantCache[Key];
//TODO: Do a look-up from the above step and then construct the metadata
var tenantMetadata = metadataSvc.GetTenantMetadata(...);
//TODO: If the data match does not happen from the Step2, build the cache and then return the value.
TenantCache.Add(key,tenantMetadata);
return tenantMetadata;
}
}
Note
The above code snippet uses the various service placeholders, cache and the other methods which will require to be used based on the designed application services. If you wish not to cache the tenant metadata, if it may contain some sensitive data, you can remove the caching implementation parts.
This implementation can be spread across all your web facing portals like your Web UI, Web Api and WebJobs etc so that it is same across all apps and it is easy to test and consume.
HTH.
Related
Having a hard time wrapping my head around how to do this. This is a multi tenant ASP MVC Core 3.1 app and the current means of resolving the user's current tenant is via claims in the cookie. I have the following in the startup.cs ConfigureServices method:
services.AddScoped<ITenantResolver, TenantResolver>();
services.AddScoped(p => p.GetService<IHttpContextAccessor>()?.HttpContext?.GetTenant());
and the following middleware registered in the Configure method:
app.UseMiddleware<TenantResolutionMiddleware>();
The process is that the middleware resolves the tenant (using the TenantResolver) and then sets it in the HttpContext. The GetTenant() extension method on the HttpContext returns the tenant to the services. Depending on the selected tenant different connections strings are used and therefore the EFCore DbContext is linked to a different database. Really the only difference when different tenants are selected are the DbContext and the Tenant object;
That all works, but what I need to be able to do is use the services outside of the request pipeline, say in a hangfire background job or in an ancillary project I need services to report on different databases in an application that does not have a request pipeline.
What I want to be able to do is something like this:
foreach(var tenant in Tenants)
{
// set the tenant
// get a service provider for that tenant and use those services
}
I know I need to be able to 'set the tenant' outside of the tenant resolution system but my mind is stuck in the request stack.
Thanks,
Brian
Do your services still use dependency injection? You can create an ITenantResolver that lets you explicitly set the tenant:
class TenantResolver : ITenantResolver
{
// Not sure what type 'Tenant' is, but you can change it to suit
public string Tenant { get; set; }
string GetTenant() => Tenant;
}
Then in your service's main execution loop:
var factory = services.GetService<IServiceScopeFactory>();
foreach (var tenant in Tenants)
{
using (var scope = factory.CreateScope())
{
scope.Services.GetService<TenantResolver>().Tenant = tenant;
// Use scope.Services to resolve services using the current tenant
}
}
Background
I've been following the documentation for using IdentityServer4 with single-page-applications on ASP.NET-Core 3.1 and as such created a project via the dotnet new react -au Individual command.
This creates a project which uses the Microsoft.AspNetCore.ApiAuthorization.IdentityServer NuGet package.
So far it's been really great and it got token-based authentication for my ReactJS application working without any pain!
From my ReactJS application, I can access the user information populated by the oidc-client npm package such as the username.
Also, calls to my Web APIs with the [Authorize] attribute work as expected: only calls with a valid JWT access token in the request header have access to the API.
Problem
I'm now trying to access basic user information (specifically username) from within a GraphQL mutation resolver via an injected IHttpContextAccessor but the only user information I can find are the following claims under IHttpContextAccessor.HttpContext.User:
nbf: 1600012246
exp: 1600015846
iss: https://localhost:44348
aud: MySite.HostAPI
client_id: MySite
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier: (actual user GUID here)
auth_time: 1600012235
http://schemas.microsoft.com/identity/claims/identityprovider: local
scope: openid
scope: profile
scope: MySite.HostAPI
http://schemas.microsoft.com/claims/authnmethodsreferences: pwd
The same issue happens for Web API controllers as well.
Details
MySite is the namespace of my solution and is also what I have defined as a client in my appsettings.json file:
{
"IdentityServer": {
"Clients": {
"MySite": {
"Profile": "IdentityServerSPA"
}
}
}
}
My web application project's name is MySite.Host so MySite.HostAPI the name of the resource and scope that are automatically generated by calling AuthenticationBuilder.AddIdentityServerJwt().
... this method registers an <<ApplicationName>>API API resource with IdentityServer with a default scope of <<ApplicationName>>API and configures the JWT Bearer token middleware to validate tokens issued by IdentityServer for the app.
Research
According to a few answers on Stack Overflow, adding IdentityResources.Profile() resource via IIdentityServerBuilder.AddInMemoryIdentityResources() should do the trick but it looks like it's already available via the claims I posted above (scope: profile).
I nevertheless tried it but the result is that the authentication flow becomes broken: the redirect to the login page does not work.
All of the answers I've found also make a reference to a Config class like in this demo file which holds configurations that are mainly fed to IIdentityServerBuild.AddInMemory...() methods.
However, it seems that Microsoft.AspNetCore.ApiAuthorization.IdentityServer does most of this in its implementation and instead offers extendable builders to use.
From the IdentityServer documentation, I don't believe I need to add a Client because the access token already exists. The client ReactJS application uses the access_token from oidc-client to make authorised calls to my Web APIs.
It also doesn't appear like I need to add a Resource or Scope for the username information because I believe these already exist and are named profile. More to this point is that the documentation for "IdentityServerSPA" client profile states that:
The set of scopes includes the openid, profile, and every scope defined for the APIs in the app.
I also looked at implementing IProfileService because according to the documentation this is where additional claims are populated. The default implementation is currently being used to populate the claims that are being requested by the ProfileDataRequestContext.RequestedClaimTypes object and this mechanism already works because this is how the ReactJS client code receives them. This means that when I'm trying to get the user claims from ASP.NET-Core Identity, it's not properly populating ProfileDataRequestContext.RequestedClaimTypes or perhaps not even calling IProfileServices.GetProfileDataAsync at all.
Question
Considering that my project uses Microsoft.AspNetCore.ApiAuthorization.IdentityServer, how can I view the username from my ASP.NET-Core C# code, preferably with IHttpContextAccessor?
What you need to do is to extend the default claims requested by IdentityServer with your custom ones. Unfortunately, since you're using the minimalistic IdentityServer implementation by Microsoft, the correct way of making the client request the claims isn't easy to find. However, assuming you have only one application (as per the template), you could say that the client always wants some custom claims.
Very important first step:
Given your custom IProfileService called, say, CustomProfileService, after these lines:
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
you have to get rid of the implementation used in the scaffolded template, and use your own:
services.RemoveAll<IProfileService>();
services.AddScoped<IProfileService, CustomProfileService>();
Next, the actual implementation of the custom IProfileService isn't really hard if you start from Microsoft's version:
public class CustomProfileService : IdentityServer4.AspNetIdentity.ProfileService<ApplicationUser>
{
public CustomProfileService(UserManager<ApplicationUser> userManager,
IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory) : base(userManager, claimsFactory)
{
}
public CustomProfileService(UserManager<ApplicationUser> userManager,
IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory,
ILogger<ProfileService<ApplicationUser>> logger) : base(userManager, claimsFactory, logger)
{
}
public override async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
string sub = context.Subject?.GetSubjectId();
if (sub == null)
{
throw new Exception("No sub claim present");
}
var user = await UserManager.FindByIdAsync(sub);
if (user == null)
{
Logger?.LogWarning("No user found matching subject Id: {0}", sub);
return;
}
var claimsPrincipal = await ClaimsFactory.CreateAsync(user);
if (claimsPrincipal == null)
{
throw new Exception("ClaimsFactory failed to create a principal");
}
context.AddRequestedClaims(claimsPrincipal.Claims);
}
}
With those two steps in place, you can start tweaking CustomProfileService's GetProfileDataAsync according to your needs. Notice that ASP.NET Core Identity by default already has the email and the username (you can see these in the claimsPrincipal variable) claims, so it's a matter of "requesting" them:
// ....
// also notice that the default client in the template does not request any claim type,
// so you could just override if you want
context.RequestedClaimTypes = context.RequestedClaimTypes.Union(new[] { "email" }).ToList();
context.AddRequestedClaims(claimsPrincipal.Claims);
And if you want to add custom data, for example, the users first and last name:
// ....
context.RequestedClaimTypes = context.RequestedClaimTypes.Union(new[] { "first_name", "last_name" }).ToList();
context.AddRequestedClaims(claimsPrincipal.Claims);
context.AddRequestedClaims(new[]
{
new Claim("first_name", user.FirstName),
new Claim("last_name", user.LastName),
});
User information can be retrieved via the scoped UserManager<ApplicationUser> service which is set up by the project template. The users's claims contains "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" (ClaimTypes.NameIdentifier) whose value is the user identifier. UserManager<>.FindByIdAsync() can then be used to retrieve the ApplicationUser associated with the user and which contains additional user information.
Note that this contacts the user store each time it's invoked. A better solution would be to have the extra user information in the claims.
First, explicitly add the IHttpContextAccessor service if you haven't already by calling services.AddHttpContextAccessor();
From within an arbitrary singleton service:
public class MyService
{
public MyService(
IHttpContextAccessor httpContextAccessor,
IServiceProvider serviceProvider
)
{
var nameIdentifier = httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
using (var scope = serviceProvider.CreateScope())
{
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var user = await userManager.FindByIdAsync(nameIdentifier);
// Can access user.UserName.
}
}
}
UserManager<ApplicationUser> can be accessed directly within Razor pages and Controllers because these are already scoped.
I am looking for a solution/suggestion that helps me creating permission based access to web api endpoints/controller actions.
Role based access is not suitable becuase I don't have fixed rules that I could use in code like Role("Admin") oder Role("Controller").
Claim based permissions is also not feasable because each user/client can have different permissions on each business object/entity (e.g. Read/Write-access to own tickets and read access to all ticket of his/her company or if its a technician of my company full access to all tickets of all customers. So each user would have 10s or even hundrets of claims which I would have to evaluate at each access of my API.
It is some kind of multi tenancy in just on database and the tenants are our customers with some kind of "master tenant" that has access to all of the tenant data.
I think that something like Visual Guard would satisfy my needs but it is pretty expensive and they don't support net core for now and their documentation seems pretty outdated.
I don't need a usable solution at once but some hints and tricks how I could achieve that would very much be apprieciated because I am looking and searching for some time now.
Details on "database permissions":
What I mean is in my frontend (Winforms app) I want to establish a security system where I can create and assign roles to users and in those roles is defined which actions a user can execute and which CRUD operations he/she can do on specific business objects. Each role can have n users and each role can have n permissions. Each permission on itself declares for exmaple Create:false, Read:true, Write:true and Delete:false. If a permission for a specific business object is not found CRUDs on that BO is denied totally.
So whenever an action in my API is called I have to check if that user and his/her rule allows him to do that specific action based on rules and permissions in my database.
Details an application structure:
Frontend will be a Winforms app which calls the API in the background by OData. I don't want to rely solely on security in the Winforms app because the API will be accessible from the internet and I can't be sure if a user would not try to access the api with his credentials just to see what is possblie without the "frontend filter". So the permissions lie in the API and if a user tries to access s.t. in the frontend app the app itself "asks" the API if that is possible.
Later on I want to create mobile clients that also use the Odata Web API.
The relevant API in asp.net core are:
IAuthorizationService
AuthorizationPolicy
IAuhtorizationRequirement
IAuthorizationHandler
The authorization pattern you are looking for is called Resource-based authorization
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/resourcebased?view=aspnetcore-2.2
Basically, you can define AuthorizationPolicy, and apply it to a instance of a resource:
var ticket = _ticketRepository.Find(ticketID);
var authorizationResult = await _authorizationService
.AuthorizeAsync(User, ticket, "EditTicketPolicy");
In the authorization handler, you can check if the user is the owner of the resource.
public class ResourceOwnerRequirement : IAuthorizationRequirement
{
}
public class ResourceOwnerHandler
: AuthorizationHandler<ResourceOwnerRequirement, MyBusinessObject>
//: AuthorizationHandler<ResourceOwnerRequirement> use this overload to handle all types of resources...
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ResourceOwnerRequirement requirement,
MyBusinessObject resource)
{
int createdByUserId = resource.CreatedBy;
Claim userIdClaim = ((ClaimsIdentity)context.User.Identity).FindFirst("UserId");
if (int.TryParse(userIdClaim.Value, out int userId)
&& createdByUserId == userId)
{
context.Succeed(requirement);
}
}
}
//admin can do anything
public class AdminRequirementHandler : IAuthorizationHandler
{
public Task HandleAsync(AuthorizationHandlerContext context)
{
if (context.User.Claims.Any(c => c.Type == "Role" && c.Value == "Administator"))
{
while (context.PendingRequirements.Any())
{
context.Succeed(context.PendingRequirements.First());
}
}
return Task.CompletedTask;
}
}
BTW, this still can be called claims or role based authorization. Users with specific role can edit their own tickets, but users with admin role also other tickets. The difference is that you apply authorization to a resource, not just action
EDIT:
I need to apply filtering by requesting host name on all database calls in my Web API service.
This filtering works like so:
lookup the Site to profile against based on the requesting hostname
Apply Site.Id on all subsequent data access calls made in the request
Essentially a global filter so so that data returned by the API service is always contained to the host.
One solution would be to pass the host name in as an argument on all my service methods like so:
public IEnumerable<Profiles> GetProfiles ()
{
var host = HttpContext.Current.Request.ServerVariables["SERVER_NAME"];
return profilesService.Get(host);
}
But since this is a consistent rule on all requests I would like to come up with a more elegant way to handle this so my service calls are just profileSerivce.Get();
I think I need to inject a ISiteLocator into my service layer that has either the host name or even better the Id already retrieved from the database that I can then apply. But I'm struggling with this on how and where I can reference the HttpContext to get the host name and also if it was possible to optimise this using StructureMap lifecycles.
I think I need to inject a ISiteLocator into my service layer
It seems to me you are heading into the right direction.
I'm struggling with this on how and where I can reference the
HttpContext
This is really simple actually. Define the ISiteLocator in your business layer and define an AspNetSiteLocator implementation into your ASP.NET web application, preferably close to (or inside) your Composition Root. That implementation might look like this:
public class AspNetSiteLocator : ISiteLocator
{
private readonly ISiteRepository siteRepository;
public AspNetSiteLocator(ISiteRepository siteRepository)
{
this.siteRepository = siteRepository;
}
Site ISiteLocator.GetCurrentSite()
{
return this.siteRepository.GetById(CurrentHostName);
}
private static string CurrentHostName
{
get
{
return HttpContext.Current.Request
.ServerVariables["SERVER_NAME"];
}
}
}
that has either the host name or even better the Id already retrieved
from the database
Try to let your ISiteLocator return data in a way that is most convenient to the consumers of that locator. In my example I returned a Site entity (if you have such an entity in your domain). This is probably more convenient than the host name or the Id, since consumers possibly have to query for the site again thereselves. However, perhaps Id is the most convenient, but that's up to you to find out.
how [...] to optimise this using StructureMap lifecycles
The implementation above doesn't have any state, so it can be registered with any lifetime; singleton for instance. However, every call to ISiteLocator.GetCurrentSite() will result in a new request to the ISiteRepository, which can cause too much overhead. In that case you probably want an implementation that stores the Site in a private field and always returns that instance. In that case you shoud register that implementation on a 'Per Web Request' basis (since the SERVER_NAME will not change during the request).
I've a SecurityService that has a AutoLogin method, that uses the ServiceSecurityContext to find out which windows identity is calling and then tries to find the related user account in the database. This is working fine when it is called from a web site that uses impersonation and requires integrated security in IIS. The call is using the stock NetPipeBinding.
I'd like to test the service as follows:
[TestMethod]
public void AutoLoginAsAnonymousFails()
{
using (var anonymousContext = WindowsIdentity.Impersonate(WindowsIdentity.GetAnonymous().Token))
{
ISecurityService securityService = ClientChannelManager.CreateSecurityServiceChannel();
var loginResponse = securityService.AutoLogin();
((ICommunicationObject)securityService).Close();
Assert.IsFalse(loginResponse.IsSuccessful);
}
}
On the service side the user in the securitycontext is always me - how to make it an anonymous user? I've already tried to impersonate the IntPtr.Zero but without success.
For reference the relevant part of the service method:
public ResponseMessage AutoLogin()
{
if (ServiceSecurityContext.Current.WindowsIdentity != null
&& !ServiceSecurityContext.Current.WindowsIdentity.IsAnonymous
&& !ServiceSecurityContext.Current.WindowsIdentity.IsGuest
&& ServiceSecurityContext.Current.WindowsIdentity.IsAuthenticated)
{
// find the user based on his windows identity and return success = true message
}
// return success = false message
}
This is a classic example of how separation of concerns could help you. Instead of relying directly on ServiceSecurityContext (something that one should, IMO, never do), make sure to configure your service so that security information is instead encapsulated in Thread.CurrentPrincipal.
IIRC, when you use Windows authentication and impersonation, it may even set this up for you automatically, but otherwise, you can always write a custom ServiceAuthorizationManager, that does this for you.
This will allow you to vary your security concerns independently of your domain logic. If you stick with the IPrincipal interface and resist the temptation of downcasting to WindowsPrincipal, your code will even be ready for the future of identity: Claims-based Identity, as implemented by the Windows Identity Foundation (WIF).
This also helps tremenduously with unit testing, because you can then just assign a GenericPrincipal to Thread.CurrentPrincipal before invoking your System Under Test (SUT).