abp-samples icon indicating copy to clipboard operation
abp-samples copied to clipboard

Missing NG-TIERED OpenIddict DomainTenantResolver Example

Open jryanamcs opened this issue 2 years ago • 10 comments

Can an NG-TIERED OpenIddict DomainTenantResolver example be added under https://github.com/abpframework/abp-samples/tree/master/DomainTenantResolver/OpenIddict please.

jryanamcs avatar Feb 07 '23 21:02 jryanamcs

@maliming in case you are busy to make full sample code, please provide some key guideline here. My project stuck here same this guy https://support.abp.io/QA/Questions/5255/Issue-with-DomainTenantResolver-and-constant-string-in-subdomain

My specs:

  • ABP Framework version: v7.2.2
  • UI type: Angular
  • DB provider: EF Core (Mysql)
  • Tiered Identity Server Separated (Angular): yes

Problem

When i access http://tenant1-ngs.mydomain.com everything working as epxected (api, ids, app reslove teanant = tenant1) But when access http://ngs.mydomain.com i got issue

An error has occurred!
Http failure response for http://{0}-apis.mydomain.com:44394/api/abp/application-configuration: 0 Unknown Error

Expected result: when access http://ngs.mydomain.com angular app will call api http://apis.mydomain.com:44394/api/abp/application-configuration

Here my steps on win:

  1. Add test host to C:\Windows\System32\drivers\etc\hosts

    • 127.0.0.1 ngs.mydomain.com tenant1-ngs.mydomain.com
    • 127.0.0.1 apis.mydomain.com tenant1-apis.mydomain.com
    • 127.0.0.1 ids.mydomain.com tenant1-ids.mydomain.com
  2. AuthServer Edit code aspnet-core/src/MyCompany.AuthServer/MyCompanyAuthServerModule.cs

    MyCompanyAuthServerModule::PreConfigureServices()

    ...
    PreConfigure<AbpOpenIddictWildcardDomainOptions>(options => {
         options.EnableWildcardDomainSupport = true;
         options.WildcardDomainsFormat.Add("http://{0}-ngs.mydomain.com:4200");
         options.WildcardDomainsFormat.Add("http://{0}-apis.mydomain.com:44394");
    });
    ...
    

    MyCompanyAuthServerModule::ConfigureServices()

    ...
    options.TokenValidationParameters.IssuerValidator = TokenWildcardIssuerValidator.IssuerValidator;
    options.TokenValidationParameters.ValidIssuers = new[] {
        "http://ids.mydomaincom:44316/",
        "http://{0}-ids.mydomain.com:44316/"
    };
    ...
    
  3. Edit MyCompany.HttpApi.Host Edit code aspnet-core/src/MyCompany.HttpApi.Host/MyCompanyHttpApiHostModule.cs

    private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
     {
         context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
             .AddJwtBearer(options =>
             {
                 options.Authority = configuration["AuthServer:Authority"];
                 options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]);
                 options.Audience = "MyCompany";
    
                 options.TokenValidationParameters.IssuerValidator = TokenWildcardIssuerValidator.IssuerValidator;
                 options.TokenValidationParameters.ValidIssuers = new[] {
                     "http://ids.mydomaincom:44316/",
                     "http://{0}-ids.mydomain.com:44316/"
                 };
             });
    
         Configure<AbpTenantResolveOptions>(options => {
              options.AddDomainTenantResolver("{0}-apis.mydomain.com:44394");
         });
     }
    
  4. Angular env setting

    angular/src/environments/environment.ts file content

    import { Environment } from '@abp/ng.core';
    
    const baseUrl = 'http://{0}-ngs.mydomain.com:4200';
    
    const oAuthConfig = {
      issuer: 'http://{0}-ids.mydomain.com:44316/',
      redirectUri: baseUrl,
      clientId: 'PortX_App',
      responseType: 'code',
      scope: 'offline_access PortX',
      requireHttps: false,
      skipIssuerCheck: true,
    };
    
    export const environment = {
      production: false,
      application: {
        baseUrl,
        name: 'PortX',
      },
      oAuthConfig,
      apis: {
        default: {
          url: 'http://{0}-apis.mydomain.com:44394',
          rootNamespace: 'PortX',
        },
        AbpAccountPublic: {
          url: oAuthConfig.issuer,
          rootNamespace: 'AbpAccountPublic',
        },
      },
    } as Environment;
    
  5. On chrome browser access

    • http://tenant1-ngs.mydomain.com => working as expected ( ids, apps, apis resolve to tenant tenant1)
    • http://ngs.mydomain.com => failed on call xhr http://{0}-apis.mydomain.com:44394/api/abp/application-configuration ( expected resolve to host app so need call xhr http://apis.mydomain.com:44394/api/abp/application-configuration )

Confirmed

  • http://ids.mydomain.com => working as expected (host app)
  • http://tenant1-ids.mydomain.com => working as expected (tenant1 app)
  • http://apis.mydomain.com:44394/api/abp/application-configuration ( response correct host app setting)
  • http://tenant1-apis.mydomain.com:44394/api/abp/application-configuration ( response correct teant1 app setting)

hungtrinh avatar Jul 12 '23 03:07 hungtrinh

hi

What is the problem/error/warning you got now?

maliming avatar Jul 12 '23 05:07 maliming

@maliming I update my comment please review it https://github.com/abpframework/abp-samples/issues/223#issuecomment-1631804862 . Look like angular app can not resolve to host app resource when apply subdomain tenant resolver code. Thanks

hungtrinh avatar Jul 12 '23 07:07 hungtrinh

hi

This seems an angular issue.

Can you try another URL format?

http://{0}-ids.mydomain.com:44316/ to http://{0}.ids.mydomain.com:44316/

maliming avatar Jul 12 '23 07:07 maliming

@maliming thanks let me try again with http://{0}.ids.mydomain.com:44316/ and feedback late

BTW, I prefer pattern {0}-ids {0}-apps {0}-ngs than {0}.something because now My ssl certificate issued for *.mydomain.com. If use http://{0}.ids.mydomain.com:44316/ may be i need issue ssl cerfiticate for domain http://*.ids.mydomain, http://*.ngs.mydomain, http://*.apis.mydomain too

hungtrinh avatar Jul 12 '23 07:07 hungtrinh

Let's confirm the problem first, then we will fix it.

maliming avatar Jul 12 '23 08:07 maliming

@maliming http://{0}-ids.mydomain.com:44316/ to http://{0}.ids.mydomain.com:44316/ then Angular app working as expected

  • http://ngs.mydomain.com/ => working as expected (host app)
  • http://tenant1.ngs.mydomain.com/ => working as expected (tenant1 app)

Relative Issue (Swagger Api App)

  1. http://apis.mydomain.com/swagger/index.html click authorize button will redirect to http://ids.portx-test.com:44316/Account/Login/.... (As Expected)
  2. http://tenant1.apis.mydomain.com/swagger/index.html click authorize button will redirect to http://ids.portx-test.com:44316/Account/Login/.... (Wrong, Expected Uri http://tenant1.ids.portx-test.com:44316/Account/Login/....)

In 2 case above i got same overlay setting when click authorize button

Available authorizations

Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.

API requires the following scopes. Select which ones you want to grant to Swagger UI.

oauth2 (OAuth2, authorizationCode)
Authorization URL: http://ids.mydomain.com:44316/connect/authorize

Token URL: http://ids.mydomain.com:44316/connect/token

Flow: authorizationCode

client_id:

When load http://tenant1.apis.mydomain.com/swagger/index.html i see xhr http://tenant1.apis.mydomain.com:44394/swagger/v1/swagger.json response json

{
  "openapi": "3.0.1",
  ...
  "components": {
    "securitySchemes": {
      "oauth2": {
        "type": "oauth2",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "http://ids.mydomain.com:44316/connect/authorize",
            "tokenUrl": "http://ids.mydomain.com:44316/connect/token",
            "scopes": {
              "PortX": "PortX API"
            }
          }
        }
      }
    }
  },
  ....
}

@maliming Do you know the way config swagger.json response support subdomain resolver? please help me. Thanks

hungtrinh avatar Jul 12 '23 10:07 hungtrinh

hi

You can try with that:

app.UseSwagger(options =>
{
    options.PreSerializeFilters.Add((swaggerDoc, httpReq) =>
    {
        var currentTenant = httpReq.HttpContext.RequestServices.GetRequiredService<ICurrentTenant>();
        if (currentTenant.IsAvailable)
        {
            foreach (var securityScheme in swaggerDoc.Components.SecuritySchemes)
            {
                securityScheme.Value.Flows.AuthorizationCode.AuthorizationUrl = new Uri($"https://{currentTenant.Name}.localhost:44301/connect/authorize");
                securityScheme.Value.Flows.AuthorizationCode.TokenUrl = new Uri($"https://{currentTenant.Name}.localhost:44301/connect/token");
            }
        }
    });
});

maliming avatar Jul 12 '23 13:07 maliming

@maliming work like a charm ^^ You are my hero.

hi

You can try with that:

app.UseSwagger(options =>
{
    options.PreSerializeFilters.Add((swaggerDoc, httpReq) =>
    {
        var currentTenant = httpReq.HttpContext.RequestServices.GetRequiredService<ICurrentTenant>();
        if (currentTenant.IsAvailable)
        {
            foreach (var securityScheme in swaggerDoc.Components.SecuritySchemes)
            {
                securityScheme.Value.Flows.AuthorizationCode.AuthorizationUrl = new Uri($"https://{currentTenant.Name}.localhost:44301/connect/authorize");
                securityScheme.Value.Flows.AuthorizationCode.TokenUrl = new Uri($"https://{currentTenant.Name}.localhost:44301/connect/token");
            }
        }
    });
});

hungtrinh avatar Jul 12 '23 16:07 hungtrinh

hi

You can try with that:

app.UseSwagger(options =>
{
    options.PreSerializeFilters.Add((swaggerDoc, httpReq) =>
    {
        var currentTenant = httpReq.HttpContext.RequestServices.GetRequiredService<ICurrentTenant>();
        if (currentTenant.IsAvailable)
        {
            foreach (var securityScheme in swaggerDoc.Components.SecuritySchemes)
            {
                securityScheme.Value.Flows.AuthorizationCode.AuthorizationUrl = new Uri($"https://{currentTenant.Name}.localhost:44301/connect/authorize");
                securityScheme.Value.Flows.AuthorizationCode.TokenUrl = new Uri($"https://{currentTenant.Name}.localhost:44301/connect/token");
            }
        }
    });
});

Tiny issue

@maliming After make more test, with above snippet i found tiny issue

  • Browser 1 access http://tenant1.apis.mydomain.com then response swagger.json correct
    • "authorizationUrl": "http://tenant1.ids.mydomain.com:44316/connect/authorize".
    • "tokenUrl": "http://tenant1.ids.mydomain.com:44316/connect/token".
  • Browser 2 access http://apis.mydomain.com then response swagger.json with wrong value (return same above)
    • "authorizationUrl": "http://tenant1.ids.mydomain.com:44316/connect/authorize". (expected uri without tenant1.)
    • "tokenUrl": "http://tenant1.ids.mydomain.com:44316/connect/token". (expected uri without tenant1.)

Look like above middleware code act as set global state for securityScheme.Value.Flows.AuthorizationCode.AuthorizationUrl and securityScheme.Value.Flows.AuthorizationCode.TokenUrl

Solution

So i keep my snippet code here for somebody needed (Please let me know if have better solution)

Edit code MyCompanyHttpApiHostModule.cs (to keep back compatible with app don't use subdomain tenant resolver I use ENV) set ENV before run api host app

  • export App__AuthTenantResolver = "{0}.ids.mydomain.com:44316"
  • export AuthServer__Authority = "http://ids.mydomain.com:44316"
app.UseSwagger(options =>
{
    options.PreSerializeFilters.Add((swaggerDoc, httpReq) =>
    {
        var currentTenant = httpReq.HttpContext.RequestServices.GetRequiredService<ICurrentTenant>();
        var configuration = httpReq.HttpContext.RequestServices.GetRequiredService<IConfiguration>();

        var authTenantResolver = configuration["App:AuthTenantResolver"]?.Trim() ?? "";
        var authority = configuration["AuthServer:Authority"]?.Trim() ?? "";
        var isTenantResolverPattern = authTenantResolver.Contains("{0}");
        var authorityScheme = authority.Split("://")[0];
        var isSubdomainTenantResolverUsed = currentTenant.IsAvailable && isTenantResolverPattern && authorityScheme != "";
        
        var authorizationUrl = isSubdomainTenantResolverUsed 
            ? new Uri($"{authorityScheme}://{string.Format(authTenantResolver, currentTenant.Name)}/connect/authorize")
            : new Uri($"{authority}/connect/authorize");
        var tokenUrl = isSubdomainTenantResolverUsed 
            ? new Uri($"{authorityScheme}://{string.Format(authTenantResolver, currentTenant.Name)}/connect/token")
            : new Uri($"{authority}/connect/token");
        

        foreach (var securityScheme in swaggerDoc.Components.SecuritySchemes)
        {
            securityScheme.Value.Flows.AuthorizationCode.AuthorizationUrl = authorizationUrl;
            securityScheme.Value.Flows.AuthorizationCode.TokenUrl = tokenUrl;
        }
    });
});

hungtrinh avatar Jul 14 '23 07:07 hungtrinh