419.2 Security and microservices of ABP framework applications - chempkovsky/CS82ANGULAR GitHub Wiki

Notes

The Abp developers have stopped providing a way to generate a solution with the "--separate-auth-server" flag for Community Edition. So, following their policy, we cannot explain how to configure the resource server for use with Abp's standalone authentication server. The only thing we can say is that it is still possible for Abp version 9.2.1. Let's assume you know how to do it.

Generating two solutions

  • Create a folder for both solutions, run cmd.exe and in the command line terminal make this folder active.
    • In the command line terminal run two commands
abp new company.abpauth --template app --ui-framework angular --theme leptonx-lite                   --skip-migrator --create-solution-folder --dont-run-install-libs --no-tests
abp new company.abpapp  --template app --ui-framework angular --theme leptonx-lite --skip-migrations --skip-migrator --create-solution-folder --dont-run-install-libs --no-tests
  • company.abpauth will play the role of authentication server
    • we use --skip-migrator-flag since we are goig to replace IIdentityDataSeeder-implementation
  • company.abpapp will play the role of resource server
    • we use --skip-migrations-flag (and --skip-migrator-flag) because we are going to delete some entities from DBContext

Remove OpeIdDic module reference from resource server solution

Using Abp Studio

  • Open company.abpapp-solution with Abp studio
    • right click 'imports/Volo.Abp.OpenIddict' and choose Remove-menu item
    • right click 'imports/Volo.Abp.OpenIddict' and choose Uninstall-menu item

Using Visual Studio

  • Open company.abpapp-solution with Visual Studio
Domain project
  • Open company.abpapp.Domain.csproj-file and remove (or comment) the line
<!--
    <PackageReference Include="Volo.Abp.OpenIddict.Domain" Version="X.Y.Z" />
-->
  • Open company.abpapp.Domain\abpappDomainModule.cs-file and remove (or comment) the lines:
// using Volo.Abp.OpenIddict;  // -- delete this line
...
[DependsOn(
//    typeof(AbpOpenIddictDomainModule), // -- delete this line
)]
Domain Shared project
  • Open company.abpapp.Domain.Shared.csproj-file and remove (or comment) the line:
<!--		
		<PackageReference Include="Volo.Abp.OpenIddict.Domain.Shared" Version="X.Y.Z" />
-->
  • Open company.abpapp.Domain.Shared\abpappDomainSharedModule.cs-file and remove (or comment) the lines:
// using Volo.Abp.OpenIddict; // -- delete this line
...
[DependsOn(
//    typeof(AbpOpenIddictDomainSharedModule), // -- delete this line
    )]
EntityFrameworkCore project
  • Open company.abpapp.EntityFrameworkCore.csproj-file and remove (or comment) the line:
<!--		
		<PackageReference Include="Volo.Abp.OpenIddict.EntityFrameworkCore" Version="X.Y.Z" />
-->
  • Open company.abpapp.EntityFrameworkCore/abpappEntityFrameworkCoreModule.cs-file and remove (or comment) the lines:
// using Volo.Abp.OpenIddict.EntityFrameworkCore; // -- delete this line
...
[DependsOn(
//        typeof(AbpOpenIddictEntityFrameworkCoreModule), // -- delete this line
    )]
  • Open company.abpapp.HttpApi.Host.csproj-file and remove (or comment) the line:
<!--		
		<PackageReference Include="Volo.Abp.Account.Web.OpenIddict" Version="X.Y.Z" />
-->
Host project
  • Open company.abpapp.HttpApi.Host.csproj-file and remove (or comment) the line:
// using Microsoft.AspNetCore.Extensions.DependencyInjection; // -- delete this line
// using OpenIddict.Validation.AspNetCore;                    // -- delete this line
// using OpenIddict.Server.AspNetCore;                        // -- delete this line
// using Volo.Abp.Account.Web;                                // -- delete this line
// using Volo.Abp.OpenIddict;                                 // -- delete this line
...
[DependsOn(
//        typeof(AbpAccountWebOpenIddictModule), // -- delete this line
    )]

Modify resource server solution

  • Open company.abpapp-solution with Visual Studio

Modify Domain project

  • delete (or comment all the code) the company.abpapp.Domain/OpenIddict/OpenIddictDataSeedContributor.cs-file

Modify Host project

  • Modify the company.abpapp.HttpApi.Host\abpappHttpApiHostModule.cs-file:
    • remove (or comment all the code) the PreConfigureServices-method
Click to show the code
/*
    public override void PreConfigureServices(ServiceConfigurationContext context)
    {
        var hostingEnvironment = context.Services.GetHostingEnvironment();
        var configuration = context.Services.GetConfiguration();

        PreConfigure<OpenIddictBuilder>(builder =>
        {
            builder.AddValidation(options =>
            {
                options.AddAudiences("abpapp");
                options.UseLocalServer();
                options.UseAspNetCore();
            });
        });

        if (!hostingEnvironment.IsDevelopment())
        {
            PreConfigure<AbpOpenIddictAspNetCoreOptions>(options =>
            {
                options.AddDevelopmentEncryptionAndSigningCertificate = false;
            });

            PreConfigure<OpenIddictServerBuilder>(serverBuilder =>
            {
                serverBuilder.AddProductionEncryptionAndSigningCertificate("openiddict.pfx", configuration["AuthServer:CertificatePassPhrase"]!);
                serverBuilder.SetIssuer(new Uri(configuration["AuthServer:Authority"]!));
            });
        }
    }
*/
  • Modify the company.abpapp.HttpApi.Host\abpappHttpApiHostModule.cs-file:
    • in the ConfigureServices-method remove (or comment all the code) as shown below:
Click to show the code
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        var configuration = context.Services.GetConfiguration();
        var hostingEnvironment = context.Services.GetHostingEnvironment();

        if (!configuration.GetValue<bool>("App:DisablePII"))
        {
            Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
            Microsoft.IdentityModel.Logging.IdentityModelEventSource.LogCompleteSecurityArtifact = true;
        }

        if (!configuration.GetValue<bool>("AuthServer:RequireHttpsMetadata"))
        {
/*
            Configure<OpenIddictServerAspNetCoreOptions>(options =>
            {
                options.DisableTransportSecurityRequirement = true;
            });
*/            
        context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddAbpJwtBearer(options =>
            {
                options.Authority = configuration["AuthServer:Authority"];
                options.RequireHttpsMetadata = configuration.GetValue<bool>("AuthServer:RequireHttpsMetadata");
                options.Audience = "abpapp";
            });

            Configure<ForwardedHeadersOptions>(options =>
            {
                options.ForwardedHeaders = ForwardedHeaders.XForwardedProto;
            });
        }

        ConfigureAuthentication(context);
        ConfigureUrls(configuration);
        ConfigureBundles();
        ConfigureConventionalControllers();
        ConfigureHealthChecks(context);
        ConfigureSwagger(context, configuration);
        ConfigureVirtualFileSystem(context);
        ConfigureCors(context, configuration);
    }
  • Modify the company.abpapp.HttpApi.Host\abpappHttpApiHostModule.cs-file:
    • in the ConfigureAuthentication-method remove (or comment all the code) as shown below:
Click to show the code
    private void ConfigureAuthentication(ServiceConfigurationContext context)
    {
/*
        context.Services.ForwardIdentityAuthenticationForBearer(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
*/
        context.Services.Configure<AbpClaimsPrincipalFactoryOptions>(options =>
        {
            options.IsDynamicClaimsEnabled = true;
        });
    }
  • Modify the company.abpapp.HttpApi.Host\abpappHttpApiHostModule.cs-file:

    • in the OnApplicationInitialization-method remove (or comment) the line: // app.UseAbpOpenIddictValidation();
  • delete Pages/Index.cshtml and Pages/Index.cshtml.cs files

  • create Controllers-folder

  • create a class Controllers/HomeController-with the code

Click to show the code
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc;

namespace company.abpapp.Controllers
{
    public class HomeController : AbpController
    {
        public ActionResult Index()
        {
            return Redirect("~/swagger");
        }
    }
}
  • add reference onto two packages in the company.abpauth.HttpApi.Host.csproj-file
Click to show the code
    <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" />
    <PackageReference Include="Volo.Abp.AspNetCore.Authentication.JwtBearer" Version="X.Y.Z" />

Modify EntityFrameworkCore project

  • open the company.abpapp.EntityFrameworkCore/EntityFrameworkCore/abpappDbContext.cs-file and remove two lines:
    • // using Volo.Abp.OpenIddict.EntityFrameworkCore; at the beginning of the file
    • // builder.ConfigureOpenIddict(); in the OnModelCreating(ModelBuilder builder) {...}-method

Build Host project

  • Open company.abpapp-solution with Visual Studio
    • build company.abpapp.HttpApi.Host-project to make sure everything is okay

Add IdentityDataSeeder into two solutions

  • Open company.abpapp-solution with Visual Studio
    • add a company.abpapp\company.abpapp.Domain\Identity\IdentityDataSeederEx-class with a code
Click to show the code
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Guids;
using Volo.Abp.Identity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;

namespace company.abpapp.Identity
{
    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(IIdentityDataSeeder))]
    public class IdentityDataSeederEx : ITransientDependency, IIdentityDataSeeder
    {
        protected IGuidGenerator GuidGenerator { get; }
        protected IIdentityRoleRepository RoleRepository { get; }
        protected IIdentityUserRepository UserRepository { get; }
        protected ILookupNormalizer LookupNormalizer { get; }
        protected IdentityUserManager UserManager { get; }
        protected IdentityRoleManager RoleManager { get; }
        protected ICurrentTenant CurrentTenant { get; }
        protected IOptions<IdentityOptions> IdentityOptions { get; }

        public IdentityDataSeederEx(
            IGuidGenerator guidGenerator,
            IIdentityRoleRepository roleRepository,
            IIdentityUserRepository userRepository,
            ILookupNormalizer lookupNormalizer,
            IdentityUserManager userManager,
            IdentityRoleManager roleManager,
            ICurrentTenant currentTenant,
            IOptions<IdentityOptions> identityOptions)
        {
            GuidGenerator = guidGenerator;
            RoleRepository = roleRepository;
            UserRepository = userRepository;
            LookupNormalizer = lookupNormalizer;
            UserManager = userManager;
            RoleManager = roleManager;
            CurrentTenant = currentTenant;
            IdentityOptions = identityOptions;
        }

        [UnitOfWork]
        public virtual async Task<IdentityDataSeedResult> SeedAsync(
            string adminEmail,
            string adminPassword,
            Guid? tenantId = null,
            string? adminUserName = null)
        {
            Guid roleGuid = GuidGenerator.Create();
            Guid adminGuid = new Guid("9ca3c973-3bb5-3ade-a137-3a1abeee2030");
            if (tenantId != null)
            {
                adminGuid = GuidGenerator.Create();
            }

            Check.NotNullOrWhiteSpace(adminEmail, nameof(adminEmail));
            Check.NotNullOrWhiteSpace(adminPassword, nameof(adminPassword));

            using (CurrentTenant.Change(tenantId))
            {
                await IdentityOptions.SetAsync();

                var result = new IdentityDataSeedResult();
                //"admin" user
                if (adminUserName.IsNullOrWhiteSpace())
                {
                    adminUserName = IdentityDataSeedContributor.AdminUserNameDefaultValue;
                }
                var adminUser = await UserRepository.FindByNormalizedUserNameAsync(
                    LookupNormalizer.NormalizeName(adminUserName)
                );

                if (adminUser != null)
                {
                    return result;
                }

                adminUser = await UserRepository.FindAsync(adminGuid);
                if (adminUser != null)
                {
                    return result;
                }


                adminUser = new IdentityUser(
                    adminGuid,
                    adminUserName,
                    adminEmail,
                    tenantId
                )
                {
                    Name = adminUserName
                };

                (await UserManager.CreateAsync(adminUser, adminPassword, validatePassword: false)).CheckErrors();
                result.CreatedAdminUser = true;

                //"admin" role
                const string adminRoleName = "admin";
                var adminRole =
                    await RoleRepository.FindByNormalizedNameAsync(LookupNormalizer.NormalizeName(adminRoleName));
                if (adminRole == null)
                {
                    adminRole = await RoleRepository.FindAsync(roleGuid);
                }
                if (adminRole == null)
                {
                    adminRole = new IdentityRole(
                        roleGuid,
                        adminRoleName,
                        tenantId
                    )
                    {
                        IsStatic = true,
                        IsPublic = true
                    };

                    (await RoleManager.CreateAsync(adminRole)).CheckErrors();
                    result.CreatedAdminRole = true;
                }

                (await UserManager.AddToRoleAsync(adminUser, adminRoleName)).CheckErrors();

                return result;
            }
        }

    }
}
  • Open company.abpauth-solution with Visual Studio
    • add a company.abpauth\company.abpauth.Domain\Identity\IdentityDataSeederEx-class with the same code as above (namespace will be different).
  • IdentityDataSeeder is called in two places
    • by DbMigrator
    • when a new tenant is created
  • admin-user is created with predefined RowId. As we mentioned in the previous article Abp uses RowId (not a userName or email) at authorization time. So to login into resource server very first time we need the same RowId in both databases. This is not a code for production. This is just a hint of what to think about.

Modify appsettings of abpauth DbMigrator

  • Open company.abpauth-solution with Visual Studio
    • modify company.abpauth/company.abpauth.DbMigrator/appsettings.json-file
    • add three new items into OpenIddict/Applications list
Click to show the code
      "abpapp_Client": {
        "ClientId": "abpapp_Client",
        "RootUrl": "http://localhost:4201"
      },
      "abpapp_App": {
        "ClientId": "abpapp_App",
        "RootUrl": "https://localhost:44346/"
      },
      "abpapp_Swagger": {
        "ClientId": "abpapp_Swagger",
        "RootUrl": "https://localhost:44346/"
      }
  • We copied two items from the company.abpapp/company.abpapp.DbMigrator/appsettings.json-file. In your case the ports will be different except 4201.
    • we are going to run angular app of the 'abpapp' using port = 4201
  • We will not register "abpapp_App": {"ClientId": "abpapp_App", "RootUrl": "https://localhost:44346/"}-record untill introspection. This record represents resource service

Modify appsettings of abpapp Host

  • Open company.abpapp-solution with Visual Studio
    • modify company.abpapp/company.abpapp.HttpApi.Host/appsettings.json-file
      • AuthServer/Authority = "https://localhost:44388". It is a port of the company.abpauth-server.
      • App/CorsOrigins = "https://*.abpapp.com,http://localhost:4201". We are going to run angular app of the 'abpapp' using port = 4201

Modify appsettings of abpauth Host

Modify OpenIddictDataSeedContributor of abpauth Domain

  • Open company.abpapp-solution with Visual Studio
    • modify company.abpauth/company.abpauth.Domain/OpenIddict/OpenIddictDataSeedContributor.cs-file
      • add scope registration in the CreateScopesAsync()-method
Click to show the code
    private async Task CreateScopesAsync()
    {
        if (await _openIddictScopeRepository.FindByNameAsync("abpauth") == null)
        {
            await _scopeManager.CreateAsync(new OpenIddictScopeDescriptor {
                Name = "abpauth", DisplayName = "abpauth API", Resources = { "abpauth" }
            });
            await _scopeManager.CreateAsync(new OpenIddictScopeDescriptor
            {
                Name = "abpapp", DisplayName = "abpapp API", Resources = { "abpapp" }
            });
        }
    }
  • add two apps registration in the CreateApplicationsAsync()-method. Insert the code at the end of the method
Click to show the code
        commonScopes = new List<string> {
            OpenIddictConstants.Permissions.Scopes.Address,
            OpenIddictConstants.Permissions.Scopes.Email,
            OpenIddictConstants.Permissions.Scopes.Phone,
            OpenIddictConstants.Permissions.Scopes.Profile,
            OpenIddictConstants.Permissions.Scopes.Roles,
            "abpapp"
        };

        // Swagger Client
        swaggerClientId = configurationSection["abpapp_Swagger:ClientId"];
        if (!swaggerClientId.IsNullOrWhiteSpace())
        {
            var swaggerRootUrl = configurationSection["abpapp_Swagger:RootUrl"]?.TrimEnd('/');

            await CreateApplicationAsync(
                applicationType: OpenIddictConstants.ApplicationTypes.Web,
                name: swaggerClientId!,
                type: OpenIddictConstants.ClientTypes.Public,
                consentType: OpenIddictConstants.ConsentTypes.Implicit,
                displayName: "Abpapp Swagger Application",
                secret: null,
                grantTypes: new List<string> { OpenIddictConstants.GrantTypes.AuthorizationCode, },
                scopes: commonScopes,
                redirectUris: new List<string> { $"{swaggerRootUrl}/swagger/oauth2-redirect.html" },
                clientUri: swaggerRootUrl.EnsureEndsWith('/') + "swagger",
                logoUri: "/images/clients/swagger.svg"
            );
        }

        //Console Test / Angular Client
        consoleAndAngularClientId = configurationSection["abpapp_Client:ClientId"];
        if (!consoleAndAngularClientId.IsNullOrWhiteSpace())
        {
            var consoleAndAngularClientRootUrl = configurationSection["abpapp_Client:RootUrl"]?.TrimEnd('/');
            await CreateApplicationAsync(
                applicationType: OpenIddictConstants.ApplicationTypes.Web,
                name: consoleAndAngularClientId!,
                type: OpenIddictConstants.ClientTypes.Public,
                consentType: OpenIddictConstants.ConsentTypes.Implicit,
                displayName: "Abpapp Angular Application",
                secret: null,
                grantTypes: new List<string> {
                    OpenIddictConstants.GrantTypes.AuthorizationCode,
                    OpenIddictConstants.GrantTypes.Password,
                    OpenIddictConstants.GrantTypes.ClientCredentials,
                    OpenIddictConstants.GrantTypes.RefreshToken,
                    "LinkLogin",
                    "Impersonation"
                },
                scopes: commonScopes,
                redirectUris: new List<string> { consoleAndAngularClientRootUrl },
                postLogoutRedirectUris: new List<string> { consoleAndAngularClientRootUrl },
                clientUri: consoleAndAngularClientRootUrl,
                logoUri: "/images/clients/angular.svg"
            );
        }

Run Dbmigrator for each solution

  • build and run company.abpauth.DbMigrator
  • build and run company.abpapp.DbMigrator

Run install libs for each solution

  • run abp install-libs for each solution

modify environment of abpapp angular app

Click to show the code
 import { Environment } from '@abp/ng.core';

const baseUrl = 'http://localhost:4201';

const oAuthConfig = {
  issuer: 'https://localhost:44388/',
  redirectUri: baseUrl,
  clientId: 'abpapp_Client',
  responseType: 'code',
  scope: 'offline_access abpapp',
  requireHttps: true,
};

export const environment = {
  production: false,
  application: {
    baseUrl,
    name: 'abpapp',
  },
  oAuthConfig,
  apis: {
    default: {
      url: 'https://localhost:44346',
      rootNamespace: 'company.abpapp',
    },
    AbpAccountPublic: {
      url: oAuthConfig.issuer,
      rootNamespace: 'AbpAccountPublic',
    },
  },
} as Environment;

Launch apps

  • build and run company.abpauth.HttpApi.Host
  • build and run company.abpapp.HttpApi.Host
  • run ng serve -o --port 4201-command for angular project of abpapp-solution
  • run ng serve -o --port 4200-command for angular project of abpauth-solution. It plays the role of the back-end application to register new users.
⚠️ **GitHub.com Fallback** ⚠️