jonpsmith / authpermissions.aspnetcore Goto Github PK
View Code? Open in Web Editor NEWThis library provides extra authorization and multi-tenant features to an ASP.NET Core application.
License: MIT License
This library provides extra authorization and multi-tenant features to an ASP.NET Core application.
License: MIT License
Hi Jon, I'm trying to add your Sharding Example to my .net 7 Web App. I'm getting the following error: I can't seem to locate the issue.
System.AggregateException
HResult=0x80131500
Message=Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: AuthPermissions.AdminCode.ITenantChangeService Lifetime: Transient ImplementationType: MyProject.Web.Sharding.ShardingTenantChangeService': Unable to resolve service for type 'Microsoft.EntityFrameworkCore.DbContextOptions1[MyProject.Web.Sharding.ShardingSingleDbContext]' while attempting to activate 'MyProject.Web.Sharding.ShardingTenantChangeService'.) Source=Microsoft.Extensions.DependencyInjection StackTrace: at Microsoft.Extensions.DependencyInjection.ServiceProvider..ctor(ICollection
1 serviceDescriptors, ServiceProviderOptions options)
at Microsoft.Extensions.DependencyInjection.ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(IServiceCollection services, ServiceProviderOptions options)
at Microsoft.Extensions.Hosting.HostApplicationBuilder.Build()
at Microsoft.AspNetCore.Builder.WebApplicationBuilder.Build()
at Program.
This exception was originally thrown at this call stack:
[External Code]
Inner Exception 1:
InvalidOperationException: Error while validating the service descriptor 'ServiceType: AuthPermissions.AdminCode.ITenantChangeService Lifetime: Transient ImplementationType: MyProject.Web.Sharding.ShardingTenantChangeService': Unable to resolve service for type 'Microsoft.EntityFrameworkCore.DbContextOptions`1[MyProject.Web.Sharding.ShardingSingleDbContext]' while attempting to activate 'MyProject.Web.Sharding.ShardingTenantChangeService'.
Inner Exception 2:
InvalidOperationException: Unable to resolve service for type 'Microsoft.EntityFrameworkCore.DbContextOptions`1[MyProject.Web.Sharding.ShardingSingleDbContext]' while attempting to activate 'MyProject.Web.Sharding.ShardingTenantChangeService'.
This is my startup code. It errors on the builder.Build() line
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity(options =>
options.SignIn.RequireConfirmedAccount = false)
.AddEntityFrameworkStores();
builder.Services.AddControllersWithViews()
.AddRazorRuntimeCompilation();
builder.Services.RegisterAuthPermissions(options =>
{
options.TenantType = TenantTypes.SingleLevel | TenantTypes.AddSharding;
options.EncryptionKey = builder.Configuration[nameof(AuthPermissionsOptions.EncryptionKey)];
options.PathToFolderToLock = builder.Environment.WebRootPath;
options.SecondPartOfShardingFile = builder.Environment.EnvironmentName;
options.Configuration = builder.Configuration;
})
//NOTE: This uses the same database as the individual accounts DB
.UsingEfCoreSqlServer(connectionString)
.IndividualAccountsAuthentication()
.RegisterAddClaimToUser()
.RegisterAddClaimToUser()
.RegisterTenantChangeService()
.AddRolesPermissionsIfEmpty(MyProjectAppAuthSetupData.RolesDefinition)
.AddTenantsIfEmpty(MyProjectAppAuthSetupData.TenantDefinition)
.AddAuthUsersIfEmpty(MyProjectAppAuthSetupData.UsersRolesDefinition)
.RegisterFindUserInfoService()
.RegisterAuthenticationProviderReader()
.AddSuperUserToIndividualAccounts()
.SetupAspNetCoreAndDatabase(options =>
{
//Migrate individual account database
options.RegisterServiceToRunInJob<StartupServiceMigrateAnyDbContext>();
//Add demo users to the database (if no individual account exist)
options.RegisterServiceToRunInJob();
//Migrate the application part of the database
options.RegisterServiceToRunInJob<StartupServiceMigrateAnyDbContext<ShardingSingleDbContext>>();
//This seeds the invoice database (if empty)
options.RegisterServiceToRunInJob<StartupServiceSeedShardingDbContext>();
});
//This is used to set a tenant as "Down",
builder.Services.AddDistributedFileStoreCache(options =>
{
options.WhichVersion = FileStoreCacheVersions.Class;
//I override the the default first part of the FileStore cache file because there are many example apps in this repo
options.FirstPartOfCacheFileName = "MyProjectCacheFileStore";
}, builder.Environment);
//manually add services from the AuthPermissions.SupportCode project
builder.Services.AddSingleton<IGlobalChangeTimeService, GlobalChangeTimeService>(); //used for "update claims on a change" feature
builder.Services.AddSingleton<IDatabaseStateChangeEvent, TenantKeyOrShardChangeService>(); //triggers the "update claims on a change" feature
builder.Services.AddTransient<IAccessDatabaseInformation, AccessDatabaseInformation>();
builder.Services.AddTransient<ISetRemoveStatus, SetRemoveStatus>();
var app = builder.Build();
The current implementation of getting the claims for the new token from the ClaimsPricipal of the expired token, creates duplicate audiences and thus fails after the first token refresh.
Recalculating the claims based on the userId from the ClaimsPrincipal resolves this issue.
Hi,
I think it would be great if the library had CancellationToken support. After all, EF Core supports this already. It would just be a matter of forwarding the cancellation token to EF Core.
I am getting the following exception when attempting to login a user with email and password:
System.ArgumentException: Expression of type 'System.Threading.Tasks.Task`1[System.Collections.Generic.List`1[AuthPermissions.DataLayer.Classes.RoleToPermissions]]' cannot be used for return type 'System.Threading.Tasks.Task`1[System.Collections.Generic.IEnumerable`1[AuthPermissions.DataLayer.Classes.RoleToPermissions]]'
at System.Linq.Expressions.Expression.ValidateLambdaArgs(Type delegateType, Expression& body, ReadOnlyCollection`1 parameters, String paramName)
at System.Linq.Expressions.Expression.Lambda[TDelegate](Expression body, String name, Boolean tailCall, IEnumerable`1 parameters)
at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)
The login code is as follows:
var result = await _signInManager.PasswordSignInAsync(request.Email, request.Password, false, false);
if (result.Succeeded)
{
var user = await _userManager.FindByEmailAsync(request.Email);
var token = await _tokenBuilder.GenerateTokenAndRefreshTokenAsync(user.Id);
return Ok(new UserLoginResponse(token.Token, token.RefreshToken));
}
else
{
return BadRequest(new {Message = "Email or password is incorrect"});
}
An application using AuthP's multi-tenant feature requires a service that implements the ITenantChangeService
which is called when a tenant is created, updated, moved or deleted. The current implementation creates a transaction that covers both the AuthP DbContext and the application's DbContext.
This works, but has the following limitations:
ITenantChangeService
is quite complex for the user - it would be nice to make it simpler. One approach would have an abstract class containing a default creation of the application's DbContext, with a way to override that default creation of the DbContext if the developer needs to add extra features.Looking at the code it could be changed to a transaction on just the AuthP DbContext, and within that transaction a call to the (modified) ITenantChangeService
. If the ChangeService fails / returns an error it can roll back the AuthP changes.
For some changes, like delete and move, the ITenantChangeService
would need a transaction too. In this case it gets complex.
While AuthP can be used with any authentication provider by implementing a custom ISyncAuthenticationUsers
, when executing the finalise methods, such as SetupAspNetCoreAndDatabase
it assumes the application is using cookies and IdentityUser
which makes it unsuitable for services like Azure AD and B2C and causes the following exception.
System.AggregateException: 'Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory1[Microsoft.AspNetCore.Identity.IdentityUser] Lifetime: Scoped ImplementationType: AuthPermissions.AspNetCore.Services.AddPermissionsToUserClaims': Unable to resolve service for type 'Microsoft.AspNetCore.Identity.UserManager
1[Microsoft.AspNetCore.Identity.IdentityUser]' while attempting to activate 'AuthPermissions.AspNetCore.Services.AddPermissionsToUserClaims'.)'`
Error:
An error occurred while starting the application.
AuthPermissionsBadDataException: True:
Line/index 2, char: 1: The role App Admin wasn't found in the auth database.
App Admin
|
Line/index 1: The user [email protected] didn't have any roles.
App Admin
Line/index 3, char: 1: The role Tenant User wasn't found in the auth database.
Tenant User
|
Line/index 2: The user [email protected] didn't have any roles.
Tenant User
Line/index 4, char: 1: The role Tenant Admin wasn't found in the auth database.
Tenant Admin,Tenant User
|
Line/index 4, char: 14: The role Tenant User wasn't found in the auth database.
Tenant Admin,Tenant User
|
Line/index 3: The user [email protected] didn't have any roles.
Tenant Admin,Tenant User
Line/index 5, char: 1: The role Tenant User wasn't found in the auth database.
Tenant User
|
Line/index 4: The user [email protected] didn't have any roles.
Tenant User
Line/index 6, char: 1: The role Tenant User wasn't found in the auth database.
Tenant User
|
Line/index 5: The user [email protected] didn't have any roles.
Tenant User
Line/index 7, char: 1: The role Tenant User wasn't found in the auth database.
Tenant User
|
Line/index 6: The user [email protected] didn't have any roles.
Tenant User
Line/index 8, char: 1: The role Tenant User wasn't found in the auth database.
Tenant User
|
Line/index 7: The user [email protected] didn't have any roles.
Tenant User
AuthPermissions.CommonCode.ErrorReportingExtensions.IfErrorsTurnToException(IStatusGeneric status) in ErrorReportingExtensions.cs, line 63
AuthPermissionsBadDataException: True: Line/index 2, char: 1: The role App Admin wasn't found in the auth database. App Admin | Line/index 1: The user [email protected] didn't have any roles. App Admin Line/index 3, char: 1: The role Tenant User wasn't found in the auth database. Tenant User | Line/index 2: The user [email protected] didn't have any roles. Tenant User Line/index 4, char: 1: The role Tenant Admin wasn't found in the auth database. Tenant Admin,Tenant User | Line/index 4, char: 14: The role Tenant User wasn't found in the auth database. Tenant Admin,Tenant User | Line/index 3: The user [email protected] didn't have any roles. Tenant Admin,Tenant User Line/index 5, char: 1: The role Tenant User wasn't found in the auth database. Tenant User | Line/index 4: The user [email protected] didn't have any roles. Tenant User Line/index 6, char: 1: The role Tenant User wasn't found in the auth database. Tenant User | Line/index 5: The user [email protected] didn't have any roles. Tenant User Line/index 7, char: 1: The role Tenant User wasn't found in the auth database. Tenant User | Line/index 6: The user [email protected] didn't have any roles. Tenant User Line/index 8, char: 1: The role Tenant User wasn't found in the auth database. Tenant User | Line/index 7: The user [email protected] didn't have any roles. Tenant User
AuthPermissions.CommonCode.ErrorReportingExtensions.IfErrorsTurnToException(IStatusGeneric status) in ErrorReportingExtensions.cs
+
throw new AuthPermissionsBadDataException(status.Errors.Count() == 1
AuthPermissions.AspNetCore.HostedServices.AddRolesTenantsUsersIfEmptyOnStartup.StartAsync(CancellationToken cancellationToken) in AddRolesTenantsUsersIfEmptyOnStartup.cs
+
status.IfErrorsTurnToException();
Microsoft.Extensions.Hosting.Internal.Host.StartAsync(CancellationToken cancellationToken)
Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host)
Example3.MvcWebApp.IndividualAccounts.Program.Main(string[] args) in Program.cs
+
CreateHostBuilder(args).Build().Run();
Hey, I wrote small piece of code to handle the HasPermission
attribute fluently for minimal api.
Usage:
app.MapGet("/", () => "Hello World!")
.HasPermission(ApplicationPermission.AccessAll);
Extension method:
public static TBuilder HasPermission<TBuilder>(this TBuilder builder, object permission) where TBuilder : IEndpointConventionBuilder
{
var authorizeData = new HasPermissionAttribute(permission);
builder.RequireAuthorization(authorizeData);
return builder;
}
Which namespace you would like this method to go into? I think people who use minimal api might find it useful
What are the steps to follow if I want to covert and existing Blazor app to multi-tenant?
In my project I have a requirement to use token signing keys that have either been generated per instance of the web server or by loading a key from a certificate. Both of these scenarios produce a key that is stored in a byte[].
I have had issue encoding that byte[] into a string in a useable way, the resulting key in the token builder doesn't match the original byte[] given to the token validation parameters.
Based on the examples and the documentation I understand the intention is to load a key from a file or some sort of secrets store, and that the key is some sort of human readable string. This doesn't work when using keys generated from crypto classes like System.Security.Cryptography.Aes which produce bytes that don't encode into strings in a reversible way.
My suggestion for resolving this which wouldn't break the current API would be to add a "SigningKeyRaw" property of type byte[] to the AuthPJwtConfiguration class. This property can be used directly by the token builder and the current "SigningKey" property can be updated to encode and decode to the new property.
I can raise a PR with this change if that is convenient.
Hi @JonPSmith
I've added PostgreSQL database provider in my fork however I had to delete the migrations classes from the DataLayer folder, change the DB provider in DesignTimeContextFactory
class and generate new ones. I was wondering on how to do this dynamically. I was thinking to generate migrations for PostgreSQL under a different folder and in SetupExtensions
class apply them based on the database provider. For ex in UsingEfCoreSqlServer()
apply the migration generated for SqlServer. Is that possible? Or how would you apply the migration based on the database provider?
Thanks!
Thank you for your great library.
I hope you can share an example how I can use this library to have multi tenant api integrated with IdentityServer4
There is a problem when using the hierarchical Move feature with the current arrangement. The problem comes that logged-in users that are linked to tenants that have been moved will have the wrong DataKey. This could cause lots of problems.
The best solution is remove the DataKey claim and replace it with the tenant primary key (tenantId) in the claims. The tenantId doesn’t change with a move and you then use the tenantId to get the DataKey. The down side is getting the DataKey adds an extra database access to get the Parentkey from the tenant, which when combined with the tenantId will create the correct DataKey. To do this you would create a different IGetDataKeyFromUser
which contains a lazy DataKey which accesses the AuthP tenant to get the DataKey.
NOTE: This is a breaking change, and needs a way to transition an already running application
I've implemented the RoleChangedDetectorService and if I change the roles assigned to a user it seems to take immediate effect which is great. However, if I edit the permissions enabled in a role this doesn't seem to trigger an update to logged-in users claims.
It seems to identify users that will be impacted (i.e. users belonging to that role) but it doesn't actually cause their claims to be updated (until the user logs out and back in). Am I missing something?
Additionally, if I edit the tenant a user belongs to, the user's data key in their claim does not get updated (until logout/login).
Is this by design? The TenantKeyOrShardChangeService only looks to detect changes to the Tenant Hierarchy, not changes to an individual user's tenant.
I've implemented the various change services as per your examples, so my understanding is it shouldn't be necessary for the user to logout and log back in again for changes to their account to become effective. Specifically I've implemented: GlobalChangeTimeService, SomethingChangedCookieEvent, RoleChangedDetectorService and TenantKeyOrShardChangeService.
Thanks in advance and apologies if I'm missing something obvious.
Hi,
I am starting to design my own SaaS application using your library and I was wondering if it's possible to publish the common code like the interfaces or POCOs as its own nuget. This would get rid of dependencies when you just need the interfaces.
Thanks, really liking the library otherwise, great job on it :)
With the new multi-tenant Role types (see this explanation about multi-tenant Role types in the docs) in version 2.0.0 are two issues that haven't been covered in the current release (2.1.0). They are
RoleType
can cause problemsThe issues are
Normal
Role is changed to a HiddenFromTenant
then that Role should be removed from any tenant users.Normal
or HiddenFromTenant
Role's RoleType
are changed to TenantAutoAdd
or TenantAdminAdd
then the roles are in the correct place (i.e. should be in the TenantRoles)TenantAutoAdd
or TenantAdminAdd
Role's RoleType
are changed to Normal
or HiddenFromTenant
then the roles are in the correct place (i.e. should be in the AuthUser's Roles)I could just detect these changes and sent back an error, but issue 1 (Normal
Role is changed to a HiddenFromTenant
) would be fairly easy to delete that Role from Tenant users using the Role Delete checks.
The DeleteRoleAsync
method works on tenant Roles because it deletes the RoleToPermissions which remove the user or tenant link. The only problem is that the QueryUsersUsingThisRole
method only covers AuthUsers. We need a QueryTenantsUsingThisRole
too.
Hi
This is not a bug. i just want to verify one use-case. I have 3-4 services and a single authentication service. I need multi-tenancy support in all of them. Will this library be suitable in this scenario and where i can find examples or blogs related to this. Thanks.
Version 3.0.0 introduced a sharding approach which allows a multi-tenant application to use multiple databases to place tenant data into. The version 3.0.0 used the "ConnectionStrings" section of the appsettings file, using the names of the connection strings, e.g. "DefaultConnection" to pick the database.
However, when I tried to run the sharding example app on Azure I found some issues that would make updating the database information while the application was running pretty difficult. These issues have made me change the way of handing multiple databases, and especially how you would add (or remove) databases in production. This issue explains the problem and then describes the solution (which will be in version 3.2.0).
Connection strings contain secrets, e.g. user name and password for the database server. While can define the connection strings when you deploy, then recommended way in Azure is to set up the connection strings in the Azure App Service via the Settings -> Configuration -> Connection strings feature.
There is a way to add/change a connection string via an API in Azure, but that only works for Azure and I want the library to be used in any web server, e.g. AWS, Docker etc.
So, I assume any web server / database server will have a way to hold the database connection string in a private way. I also assume that if you are going to use multiple databases (for a sharding multi-tenant application) that one database server will have multiple databases. But I also allow for multiple database servers, say if you want some servers / database geographically spread.
Tenant
information and ends up in a claim.So when a user that it linked to a tenant logs in, then the Name of the database information is added to their claims. When the user wants to access the data in their parts of the database, then the following steps are taken:
All of these steps are handled by the IShardingConnections
service. It is possible that someone's application might need something changed, so I have added a way to replace the default ShardingConnections
code. NOTE: Until version 3.2.0 is out the links to IShardingConnections
and ShardingConnections
are using the version 3.0.0 code.
IOptionsSnapshot<T>
to access the "ConnectionStrings" and "ShardingData" sections. This means if the data is changed, then the latest data is used. The IOptionsSnapshot<T>
method is very fast.shardingsettings.json
and registered to ASP.NET Core's Configuration
. Having its own file has two benefits:
shardingsettings.json
file in production created by some admin code, and you definitely don't it overwritten when you deploy an updated version of your application - see NOTE1 and NOTE2 for how you do this.NOTE1: You need to set the shardingsettings.json
file properties shown below to stop the file from sent to the web server when you deploy your application:
NOTE2: If no shardingsettings.json
is found, then it sets up a default database, which is linked directly to the "DefaultConnection" connection string.
The GetDatabaseInfoNamesWithTenantNamesAsync
method returns a list of the names of the databases found in the shardingsettings.json file, with information on what tenants are in each database. This is useful when a tenant is created or moved as you need to select a database to hold the new tenant.
In version 3.2.0 of this library the GetDatabaseInfoNamesWithTenantNamesAsync
method returned a list where each entry contains:
In version 3.3.0 of this library the GetDatabaseInfoNamesWithTenantNamesAsync
method a third part is provided, which means the returned information is
HasOwnDb
, which provides a value that tells you:
null
: The database is emptytrue
: There is one sharding tenant in this databasefalse
: The database contains tenants that can shares a database - see NOTE1NOTE1: If database information name matches the ShardingDefaultDatabaseInfoName
held in the AuthPermissionsOptions
(default value = "Default Database") then even if there no tenants the HasOwnDb
will be false
, as that database contains the AuthP data, so its not applicable for sharding tenants.
This extra data is useful for a admin user, but the real reason of this change is because of the new GetDatabaseForNewTenant
service / method that can automatically select a database for a new tenant.
While working on Version 2 of this library I found a bug in the use of the DataKey
which can cause hierarchical multi-tenant applications to sometimes access another tenant’s data. This is therefore a critical bug.
This bug is fixed in AuthPermissions.AspNetCore 2.0.0.
BUT if you built a single or hierarchical multi-tenant application using Version 1 of this library, then you need to migrate your application's databases that use the DataKey
. This section in the [Migrate from AuthPermissions.AspNetCore 1.* to 2.0] document explains how to do this.
NOTE: Single multi-tenant applications in version 1 didn’t have a bug, but the AuthP DataKey uses the same DataKey for Single and Hierarchical multi-tenant, so you still need to follow this information when upgrading to Version 2.
Hey, I have a logical problem with IDisableJwtRefreshToken.MarkJwtRefreshTokenAsUsedAsync
.
Lets say a user logs in, and the access token is valid for 30 minutes. Then, after 10 minutes the user logs in from another machine and creates another refresh token.
Now, he logs out of the first login, and instead of disabling the first login refresh token, the second login refresh token gets disabled. The result is that he "logged out" on both machines unintendedly.
It is also a minor security vulnerability, because someone can utilize the first refresh token (it is deleted from the client side but still valid and able to produce new refresh tokens).
If you try to add a UserName to a User in Example3AppAuthSetupData.UsersRolesDefinition, you'll get an exception during the AuthDBContext migration as follows...
"AuthPermissions.BaseCode.CommonCode.AuthPermissionsBadDataException: 'Index X: The user XXXXX didn't have a userId and the IFindUserInfoService couldn't find it either.
XXXXX'"
It appears that the AuthP code is creating IdentityUsers with UserName defaulting to Email but allowing a UserName to be specified for AuthP users in the Bulk Load facility.
As a side issue, the password for new users defaults to the user's email address. Would be better to be able to set a default password from dev settings or in production randomize the initial password and force reset on initial sign-on.
Is there any way to change out the default IdentityUser class with a custom class? When I use IdentityDbContext it crashes because the library expects it to be IdentityDbContext in IndividualAccountsAddSuperUser.
Christopher
Hi,
I found your library very interesting, I would like to know if it works on net core api api with Azure B2C out of the box.
Thank you
The app-defined ITenantChangeService is not invoked during the bulk-loading process (from BulkLoadTenantsService.AddTenantsToDatabaseAsync). This leaves the app-specific database (CompanyTenant in Example 3) in an inconsistent state after new tenants are added via the API.
This may be by-design for reasons I don't fully understand. If so, please close this issue. Otherwise, I am happy to pull a PR to address this issue.
Hi Jon,
I want to know that is it possible to do same email id can be user of another tenant.
Like some application, I seen, Admin of App can invite user which is already somewhere invited or Tenant.
In AuthPermission Project, When I am trying to add it will not allow because email Id already present in AspNetUsers table.
Any suggestion on this, sir?
I haven't found any good tutorial that demonstrate how to use both JWT and Cookie based authentication in a standard way.
A demo project with detailed article would be great.
Hey, I have a question;
Why not extend microsoft's identity framework, instead of managing multiple tables in parallel?
(Ex. dbo.AspNetUsers - authp.AuthUsers)
You can use a custom user, add the roles etc, all in the same context, and then add the tenants and required extra tables and services
It makes the need of sync between those tables unnecessary, and the db much more normalized and less complex.
Great work btw,
Thanks!
If an app admin creates a new tenant user or update an existing tenant user it won't show the correct Roles when use the user admin method called GetRoleNamesForUsersAsync
. The GetRoleNamesForUsersAsync
method takes in the user's userId. This has two problems:
In the original multi-tenant application I designed the App Admin (i.e. a user manages the whole application, not linked to tenant) needed to access the data that a tenant user to understand / fix an tenant user's data.
I will add a "Access data as another tenant user" feature which will override the DataKey of the current user with the DataKey
from the selected tenant user. This provides a App Admin or an Tenant Admin to access the user's data, plus a feature useful in hierarchical multi-tenant applications.
The first thing is to find the tenant user that you want to access their data. This will use the current List AuthP's Users. The list user already has the ability to only return the users that the current user can see, e.g. App Admin can see all user, Tenant Users can only see the users in their tenant.
Once a user has been found you will call the StartAccessToUserDataAsync
method with your found user. This will create a Cookie that will contain the selected user's DataKey
which will override the default DataKey
of the current user. This Cookie should also show a prominent display showing you are in this mode. The StopAccessToUserDataAsync
method will remove the Cookie and return to normal usage.
There are two usages of this feature:
When using the package in .Net 6.0 I run into this error: Method not found: 'System.Type Microsoft.EntityFrameworkCore.Metadata.ITypeBase.get_ClrType()'.
Here is the stack:
at AuthPermissions.DataLayer.EfCode.DataKeyQueryExtension.AddHierarchicalTenantReadOnlyQueryFilter(IMutableEntityType entityData, IDataKeyFilterReadOnly dataKey) at Infrastructure.Data.AppDbContext.OnModelCreating(ModelBuilder modelBuilder) in D:\GitHub\Infrastructure\Data\AppDbContext.cs:line 58 at Microsoft.EntityFrameworkCore.Infrastructure.ModelCustomizer.Customize(ModelBuilder modelBuilder, DbContext context) at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.CreateModel(DbContext context, IConventionSetBuilder conventionSetBuilder, ModelDependencies modelDependencies) at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.GetModel(DbContext context, ModelCreationDependencies modelCreationDependencies, Boolean designTime) at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel(Boolean designTime) at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model() at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkServicesBuilder.<>c.<TryAddCoreServices>b__8_4(IServiceProvider p) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor
2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(ServiceCallSite callSite, TArgument argument) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor
2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(ServiceCallSite callSite, TArgument argument) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor
2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(ServiceCallSite callSite, TArgument argument) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor
2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(ServiceCallSite callSite, TArgument argument) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor
2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(ServiceCallSite callSite, TArgument argument) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor
2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(ServiceCallSite callSite, TArgument argument) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope) at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.<RealizeService>b__0(ServiceProviderEngineScope scope) at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope) at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType) at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType) at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider) at Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies() at Microsoft.EntityFrameworkCore.DbContext.get_ContextServices() at Microsoft.EntityFrameworkCore.DbContext.get_InternalServiceProvider() at Microsoft.EntityFrameworkCore.DbContext.Microsoft.EntityFrameworkCore.Infrastructure.IInfrastructure<System.IServiceProvider>.get_Instance() at Microsoft.EntityFrameworkCore.Infrastructure.Internal.InfrastructureExtensions.GetService[TService](IInfrastructure
1 accessor)
at Microsoft.EntityFrameworkCore.Infrastructure.AccessorExtensions.GetService[TService](IInfrastructure1 accessor) at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.get_Dependencies() at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.EnsureCreated() at Web.Program.Main(String[] args) in D:\GitHub\Web\Program.cs:line 25
Here is the line in my AppDbContext that crashes:
entityType.AddHierarchicalTenantReadOnlyQueryFilter(this);
For an entity type hierarchy (using the default setup of TPH) checking for assignability to IDataKeyFilterReadWrite and using AddSingleTenantReadWriteQueryFilter to set up the query filter does not work.
All entities in the hierarchy share a single table in the DB and AddSingleTenantReadWriteQueryFilter will try to setup an (unnamed) index on the table for each entity in the hierarchy.
I think that the query filter needs setting on each entity in the hierarchy.
However, the DataKey property tweaks and the index only need adding to the table once at top level entity that has the property.
For TPT and TPC models it is quite probably different.
Happy to have a look at implementing.
Wanted to get any thoughts you had / check it wasn't already being worked on first.
Steven
I have EF Core 7 referenced in my project and it seems that AuthP is breaking with this new version.
fail: AuthPermissions.AspNetCore.StartupServices.StartupServiceMigrateAuthPDatabase[0]
An error occurred while creating/migrating the SQL database.
System.MissingMethodException: Method not found: 'Microsoft.EntityFrameworkCore.Migrations.Operations.Builders.OperationBuilder1<Microsoft.EntityFrameworkCore.Migrations.Operations.CreateIndexOperation> Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder.CreateIndex(System.String, System.String, System.String, System.String, Boolean, System.String)'. at AuthPermissions.DataLayer.Migrations.Initial.Up(MigrationBuilder migrationBuilder) at Microsoft.EntityFrameworkCore.Migrations.Migration.BuildOperations(Action
1 buildAction)
at Microsoft.EntityFrameworkCore.Migrations.Migration.get_UpOperations()
at Microsoft.EntityFrameworkCore.Migrations.Internal.Migrator.GenerateUpSql(Migration migration, MigrationsSqlGenerationOptions options)
at Microsoft.EntityFrameworkCore.Migrations.Internal.Migrator.MigrateAsync(String targetMigration, CancellationToken cancellationToken)
at AuthPermissions.AspNetCore.StartupServices.StartupServiceMigrateAuthPDatabase.ApplyYourChangeAsync(IServiceProvider scopedServices)
It works by downgrading to EF Core 6.
Hello,
What a great, precise and clear job you did to build this multi-tenant permissions system.
I m trying to read your code an I have a question about the AuthUser and his tenant attribution. I can be competly wrong but it seems you always need to attach a "standard/normal" user to one tenant max (or tenant hierarchy).
And my question: can we imagine a case, when an external contractor will be affiliated to several companies owning a "tenant" each and in this case he will be forced to register with different userid/useremail for each tenant ? And if we choose to use external auth provider (Google/Azure other) he will not be able to register with the same auth method on serveral tenants ?
I m maybe missing something (a param or a segregation I didn't see or something) that allows this use case "horizontal multi-tenants user" ?
Thx again for this awesome github repo.
Version 1 of the AuthP library allowed you to create multi-tenant / SaaS applications and introduced the feature of a Tenant Admin who could manage users within a tenant. That reduces the work on the support people for your application.
In version 1 of AuthP library the Tenant Admin was limited to managing users in the Tenant Admin, BUT the Tenant Admin had access to every AuthP's Role (referred to a Role in this issue). This means the Tenant Admin could add roles to tenant users which gave total control over all its features, which is not a good idea.
In version 2 of the AuthP I will restrict the what a Tenant Admin can do, plus add a new feature so that you create different versions of your SaaS application (e.g. Free, Pro, Enterprise).
In version 1 of AuthP library I allowed a Tenant Admin to create / update / delete an Role, but they couldn't include advanced Permissions, which filters out advanced Permissions from a list of Permissions a Tenant Admin user. This stopped a Tenant Admin from creating a Role with an advanced Permissions.
But a problem existed in version 1 is if a Tenant Admin can create new Roles, then in Company1 could create a role that a Tenant Admin in Company2 would see. This is likely to cause confusion, especially if you have lots of tenants.
Therefore in AuthP version 2 a Tenant Admin should be barred from creating, updating or deleting a Role - only a App Admin (i.e. a user manages the whole application, not linked to tenant) can do that. The Tenant Admin's job is to manage tenant users and their Roles.
In version 2 a Role will now have a new RoleType
enum property with the following settings:
Normal
, which means the Role can be used by any user. The Tenant Admin can only see Normal
role.HiddenFromTenant
, which means the Role is not available to a user linked to a Tenant.In AuthP version 2 a Role that contains a advanced Permission will, by default, have the RoleType
property to HiddenFromTenant
. A Role can also be set to HiddenFromTenant
manually by the App Admin user.
Many SaaS applications have different versions, which allows you to get a range of income from different levels of users. In AuthP version 2 I introduce the concept of a Tenant Role.
Each Tenant will have a one-to-many link to the TenantRoles allocated to the Tenant. TenantRoles are created and added to a Tenant by the App Admin user, or it could be automated. The Tenant's TenantRoles be used in two ways:
RoleType
property is set to TenantAutoAdd
.RoleType
property is set to TenantAdminAdd
.The AuthP AzureAD handler can add a new AuthP user if the user isn't already in the AuthP user list if the AddNewUserIfNotPresent
setting is set to true. But the current version doesn't add any AuthP Roles to the new AuthP user.
In some cases you might want to provide some default AuthP Role(s) to the new user. We could achieve this adding a new AzureAD setting which holds a list of AuthP Role names that would be added to the new AuthP user.
At the moment, when you add a new hierarchical tenant you would need add any tenant Roles for the new tenant. However, it might be useful to automatically take the tenant Roles of the parent tenant?
If this was implemented, then what happens if you move a tenant - does it copies the new parent's tenant Roles? - most likely that is the correct answer.
It appears there's a lot of interest in an AuthP demo with a Blazor application.
I noticed one of the Discussions (#31), wanted to find out how to do this from a Blazor WASM application in particular.
So with @JonPSmith's blessing, I'll be taking the Example 4 project and converting it into a Blazor application.
I'll be creating
In the current version of AuthP (3.2.0) the email of a user is stored as the user provides it. But an email is NOT case sensitive. This will be a problem as Postgres is case sensative.
The simplest way to do this is add a .Lower
to the email in the AuthUser
class
.Lower
to the email in the AuthUser
class (note: do after the username has as been set). DONE.Lower
to the email in the AuthUsersAdmin FindAuthUserByEmailAsync
method. DONEhi there
i am trying to use IndividualAccountsAuthentication
which in my case class named AppUser inheriting from identity user
class AppUser : IdentityUser<int>
and i get an error
/*
Error CS0311 The type 'RivaWeb.Models.AppUser' cannot be used as type parameter 'TCustomIdentityUser' in the generic type or method 'SetupExtensions.IndividualAccountsAuthentication(AuthSetupData)'. There is no implicit reference conversion from 'RivaWeb.Models.AppUser' to 'Microsoft.AspNetCore.Identity.IdentityUser'
*/
is there something i am missing or there is no way i can use an int for the user id with this library.
I have an existing application that already uses MySQL. Can I use it with AuthPermissions library?
Hi Jon! Thank you for this amazing project and the hard work you are putting into it!
I was working on replacing IdentityUser
with ApplicationUser
in a app using AuthP, checked the wiki for what needed to be changed in my code, over here: https://github.com/JonPSmith/AuthPermissions.AspNetCore/wiki/Setup-Authentication
On point 3 it says the following:
There are some other code used in the examples that only work with UserManager (see below). If you want to use these you need to copy these too:
HostedServiceAddAspNetUsers
AspNetUserExtension, specifically ListAllAspNetUsers and CheckAddNewUserAsync
However it seems like ListAllAspNetUsers
and HostedServiceAddAspNetUsers
are no longer in the project (maybe renamed?). Just thought I'd point it out since this is a feature I'm guessing a lot of people will be/are using to extend the IdentityUser.
Have a great rest of your week! :)
Hey, I would like to help refactor the errors in a way that will enable easy translation.
We can return error code with parmeters instead of string, or use microsoft localization methods.
I've been unable to figure out how to mock authenticate in my existing sociable unit tests that use CustomWebApplicationFactory<Startup> to create a TestServer. They were working fine with role based authorization.
I tried following example 2 in Unit Test your AuthP app, and while that was working for getting services from factory.Services, it is not working when using factory.CreateClient().GetAsync("url").
I thought I could use a token created like in TestGenerateJwtTokenAsyncOk as a bearer token, but once I finally got the token generated, I got status code Unauthorized, WWW-Authenticate: Bearer error="invalid_token", error_description="The signature key was not found"
My Startup is applying the following configuration for this service:
services.RegisterAuthPermissions<Permissions>()
.AzureAdAuthentication(AzureAdSettings.AzureAdDefaultSettings(true))
//... database method left out
After that my CustomWebApplicationFactory is applying the following configuration for this service:
services.RegisterAuthPermissions<Permissions>(opt => opt.ConfigureAuthPJwtToken = CreateTestJwtSetupData())
.UsingInMemoryDatabase()
.SetupForUnitTestingAsync();
I want to be able to add Audit functionality on Tenants and related dependent entities that are created in ITenantChangeService (i.e. CreatedBy, CreatedOn, UpdatedBy, UpdatedOn, etc). In my implementation, for example, I have a Company entity associated with the tenant. When the Tenant is added or modified, I want to record audit information about when it was modified and more importantly, by whom. Currently, this is not possible without some workaround code after the tenant is created and the TenantChangeService has been executed.
One solution might be to alter ITenantChangeService methods to accept an extra parameter of AuthUserId so the editing User can be recorded on the Tenant and any dependent entities updated. For example....
ITenantChangeService.CreateNewTenantAsync(Tenant tenant**, string? UserId = null**);
AuthPermissions.SupportCode.AddUsersServices.ISignInAndCreateTenant methods could then supply the relevant UserIID during Tenant change.
Hello!
I've reading through the wiki and examples for a little while now and find that this library is a nearly perfect fit for the project I am working on. With 2 missing bits.
We are using a SQLite DB for our application as it is working in an embedded environment where it is not practical to use SQLServer. I note there isn't anything built-in for supporting full SQLite DBs, but is it possible/practical to extend that from my code? Or would it need to be an addition to the library?
We are also using GRPC for our API, I'm not sure how well this library and GRPC will integrate at the moment, has anyone had a go at this?
Would love to see this implemented but over .NET Core Identity with customization to it's classes. Any chance of that happening?
Hello.
I hope I can properly explain the issue I am having.
I am currently working in a project (.Net7) and I am using Azure Ad first time and your library also for the first time.
Since one of the requirements is the usage of MariaDB.
That was the easy part thanks to the flexibility of your library.
As least the creation of the db and the tables.
AddRolesPermissionsIfEmpty
AddTenantsIfEmpty
AddAuthUsersIfEmpty
all work.
The issue is that I keep getting 403 in postman and the console
Authorization failed. These requirements were not met:
AuthPermissions.AspNetCore.PolicyCode.PermissionRequirement
[14:23:33 INF] AuthenticationScheme: Bearer was forbidden.
when I try [HasPermission] but [Authorize] works ok.
I tried following your Example 5 but in my case I use
AddMicrosoftIdentityWebApiAuthentication
if I use AddMicrosoftIdentityWebAppAuthentication postman returns an html page
I tried using
webBuilder.Services.RegisterAuthPermissions(opt => {
opt.TenantType = TenantTypes.SingleLevel;
})
.AzureAdAuthentication(AzureAdEventSettings.AzureAdDefaultSettings(JwtBearerDefaults.AuthenticationScheme))
.UsingEfCoreMariaDb("connection")
.AddRolesPermissionsIfEmpty(ApiAuthSetupData.RolesDefinition)
.AddTenantsIfEmpty(ApiAuthSetupData.TenantDefinition)
.AddAuthUsersIfEmpty(ApiAuthSetupData.UsersRolesDefinition)
.RegisterAuthenticationProviderReader()
.SetupAspNetCoreAndDatabase();
and
webBuilder.Services
.RegisterAuthPermissions(opt => {
opt.TenantType = TenantTypes.SingleLevel;
opt.ConfigureAuthPJwtToken = new AuthPJwtConfiguration
{
Issuer = jwtData.Issuer,
Audience = jwtData.Audience,
SigningKey = jwtData.SigningKey,
TokenExpires = new TimeSpan(0, 5, 0), //Quick Token expiration because we use a refresh token
RefreshTokenExpires = new TimeSpan(1, 0, 0, 0) //Refresh token is valid for one day
};
})
.AzureAdAuthentication(AzureAdEventSettings.AzureAdDefaultSettings(JwtBearerDefaults.AuthenticationScheme))
.UsingEfCoreMariaDb("connection")
.AddRolesPermissionsIfEmpty(ApiAuthSetupData.RolesDefinition)
.AddTenantsIfEmpty(ApiAuthSetupData.TenantDefinition)
.AddAuthUsersIfEmpty(ApiAuthSetupData.UsersRolesDefinition)
.RegisterAuthenticationProviderReader()
.SetupAspNetCoreAndDatabase();
Not sure what I am missing, I hope you can point in the right direction.
Thank you.
We have a private forum with multiple chat groups (tenants) and we have an intermediate-level moderator that has access to 5 or more chat groups, with different levels of permission. He has "modify and remove" permissions in 2 chat groups and is a normal user in other chat groups. How is it possible to implement it with this library?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.