419.1 Security and microservices of ABP framework applications - chempkovsky/CS82ANGULAR GitHub Wiki
- We work only with the community edition of Abp Framework
- All tests were done for the version 9.2.1 of Abp Framework
- All tests were done for Volo.Abp.Studio.Cli (version 1.0.2), which is not Volo.Abp.Cli
- Security and microservices are closely related to each other, since each resource service must be securely protected. On the other hand, all application resource services must have a single database of users and tenants. If so, all application resource servers should communicate with the same Identity (and may be Access control) server, such as Keycloak or another home-grown solution (Single sign-on approach).
- Abp framework uses OpenIdDict as Identity server. So we will talk about this solution only (about OpenIdDict).
- Using the Volo.Abp.Studio.Cli (community edition version 1.0.2) command below, we create an application that implements two servers, one of which is an authorization server.
Click to show the code
abp new COMPANY.APPNAME --template angular --theme leptonx-lite --skip-migrations --skip-migrator --create-solution-folder --dont-run-install-libs --no-tests
- Our goal is to split this generated application into a separate oauth server and a separate resource server. Every time we add a new resource server to the application, we need to replace the oauth server code with the code to interact with the standalone oauth server.
- The main requirement is that there should be no common databases between the oauth server and any resource server.
- For thoses who wants to know what it'll be at the end we say: oauth server (after splitting) will work as authentication server only and each resource server will work as authorization server. The current implementation of the Abp framework uses memory caching, so the authorization operation (which is performed with each request to the resource server) will not result in a heavy load on the database server.
- Another surprise from the current implementation of the Abp framework community edition: during authorization (when receiving an access token), the resource server uses the RowId (Guid) of the user, not the username or the user's email address, but it uses the RowID of the table record in the database. It means that it'll be not so easy to integrate abp application with Keycloak for example. We talk about maintenance issue: suppose, today you rewrite some Abp modules, but what should you do tomorrow when Abp publishes a new version?
- Next, we want to remind important security issue (it is not related to Abp): with single sign-on approach resource servers must trust to each other. They say there is a possibility that one resource server uses the obtained access token to make a request to another resource server in order to steal data.
- EasyAbp gives ready to use sample
- The approach described below is no longer relevant. Even for older community versions it is no longer possible to generate an Abp solution with the "--separate-auth-server" flag.
- ABP OpenIddict Module-article
- There are some hints in the article: Migrating from IdentityServer to OpenIddict Step by Step Guide
- sample code is here: OpenIddict.Demo.API
- with Volo.Abp.Studio.Cli we can install Old ABP CLI with the command below.
abp install-old-cli --version 8.1.5
- We can not use 8.2.0 or higher. It does not generate the sample code we need. Now, we can run the command below with two flags that are important to us: "--separate-auth-server" and "--old".
abp new COMPANY.APPNAME --template app --ui-framework angular --separate-auth-server --theme leptonx-lite --skip-migrations --skip-migrator --create-solution-folder --dont-run-install-libs --no-tests --old
- the generated code of the resource server is as follows
Click to show the code
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddAbpJwtBearer(options =>
{
options.Authority = configuration["AuthServer:Authority"];
options.RequireHttpsMetadata = configuration.GetValue<bool>("AuthServer:RequireHttpsMetadata");
options.Audience = "APPNAME";
});
context.Services.Configure<AbpClaimsPrincipalFactoryOptions>(options =>
{
options.IsDynamicClaimsEnabled = true;
});
}- both auth server and resource serve use the same AbpDbContext (and the same connectionString) which in turn refers Volo.Abp.OpenIddict.
- The final example resource, we wanted to use, is the code generated by Volo.Abp.Cli of the version 8.1.5, but they have changed the generator template and we obtain the same code as for Volo.Abp.Studio.Cli with the "--old"-flag. At the time of writing this notes, we wanted to run the commands below to get a code example that is different from the one described above. But...
dotnet tool uninstall -g Volo.Abp.Studio.Cli
dotnet tool install -g Volo.Abp.Cli --version 8.1.5
abp new COMPANY.APPNAME8 --template app --ui-framework angular --separate-auth-server --theme leptonx-lite --skip-migrations --skip-migrator --create-solution-folder --dont-run-install-libs --no-tests
- Anyway, there were days when the generated Abp code looked like below (i.e. the Microsoft library was used directly).
- We copied the code below from our test project created a year ago. At the time of writing this article, the code has not been generated.
- This paragraph should be taken with a grain of salt.
- We copied the code below from our test project created a year ago. At the time of writing this article, the code has not been generated.
Click to show the code
context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o =>
{
o.Authority = configuration["AuthServer:Authority"];
o.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]);
o.Audience = "EnterpriseGateway";
if (o.Events == null) o.Events = new JwtBearerEvents();
o.Events.OnTokenValidated = cntxt =>
{
if (cntxt.Principal != null)
{
if (cntxt.Principal.Claims != null)
{
var scp = cntxt.Principal.Claims.Where(c => c.Type == "scope").FirstOrDefault();
if ((scp != null) && (!string.IsNullOrEmpty(scp.Value)))
{
var scopes = scp.Value.Split(' ');
if (scopes != null && (cntxt.Principal != null) && cntxt.Principal.Identity is ClaimsIdentity identity)
{
foreach (var scope in scopes)
{
identity.AddClaim(new Claim("scope", scope));
}
}
}
}
}
return Task.CompletedTask;
};
o.Events.OnMessageReceived = cntxt =>
{
return Task.CompletedTask;
};
o.Events.OnAuthenticationFailed = cntxt =>
{
return Task.CompletedTask;
};
});