- ABP Framework version: v8.0.2
- UI Type: Angular
- Database System: EF Core (MySQL)
- Tiered (for MVC) or Auth Server Separated (for Angular): no
- Exception message and full stack trace: Redirect URI not matched to the allowed list
- Steps to reproduce the issue:Enable multitenancy in abp using official documentation (https://docs.abp.io/en/abp/latest/Multi-Tenancy#domain-subdomain-tenant-resolver, https://github.com/abpframework/abp-samples/tree/master/DomainTenantResolver), create new tenant, try to login to the tenant using tenant URL.
I need assistance with an issue we're encountering with our ABP Commercial application configured for multitenancy (https://docs.abp.io/en/abp/1.0/Multi-Tenancy#domain-tenant-resolver). We have enabled multitenancy and are using OpenIddict for authentication. The configuration details for OpenIddict are stored in a database table named OpenIddictApplications, including the RedirectUris and PostLogoutRedirectUris columns.
Our application uses subdomain-based URLs for each tenant (e.g., tenant.mydomain.com). When creating a new tenant, the new tenant URL must be added to the list of allowed URIs in the RedirectUris and PostLogoutRedirectUris columns. However, this is not happening automatically, and we are facing a significant issue.
Redirect URI Mismatch Error: If the new tenant URL is not added to these lists, we receive an error indicating a mismatch of redirect URIs, preventing login and logout operations. No Support for Wildcards: The redirection endpoint URI must be an absolute URI as defined by [RFC3986] Section 4.3 (https://www.rfc-editor.org/rfc/rfc3986#section-4.3), and therefore we cannot use wildcard values like https://*.mydomain.com or https://{0}.mydomain.com. Given these constraints, we cannot dynamically handle new tenant URLs without manual updates to the allowed URIs lists.
Could you please guide how to resolve this issue? Specifically, we are looking for a solution that allows us to manage tenant-specific redirect URIs more dynamically without requiring manual updates to the database for each new tenant.
Any suggestions or best practices to handle this situation effectively would be greatly appreciated.
10 Answer(s)
-
0
hi
Can you try that?
PreConfigure<AbpOpenIddictWildcardDomainOptions>(options => { options.EnableWildcardDomainSupport = true; options.WildcardDomainsFormat.Add("https://{0}.web.getabp.net:44303/signin-oidc"); options.WildcardDomainsFormat.Add("https://{0}.web.getabp.net:44303/signout-callback-oidc"); });
Configure<OpenIddictServerOptions>(options => { options.TokenValidationParameters.IssuerValidator = TokenWildcardIssuerValidator.IssuerValidator; options.TokenValidationParameters.ValidIssuers = new[] { "https://ids.getabp.net:44301/", "https://{0}.ids.getabp.net:44301/" }; });
https://github.com/abpframework/abp-samples/blob/master/DomainTenantResolver/OpenIddict/MVC-TIERED/src/Acme.BookStore.AuthServer/BookStoreIdentityServerModule.cs#L65-L70 https://github.com/abpframework/abp-samples/blob/master/DomainTenantResolver/OpenIddict/MVC-TIERED/src/Acme.BookStore.AuthServer/BookStoreIdentityServerModule.cs#L83-L91
-
0
Hi,
Thanks for your reply.
I have already implemented the provided configuration in our project at the time of creating this ticket. However, this solution does not resolve the issue.
Could you please provide further assistance or an alternative approach to dynamically handle tenant-specific redirect URIs?
Thanks in advance
-
0
hi
Redirect URI Mismatch Error: If the new tenant URL is not added to these lists, we receive an error indicating a mismatch of redirect URIs, preventing login and logout operations.
Please share the logs of this case, Thanks.
-
0
Hi, please see the logs below:
20:03:07 [18:03:07 INF] Request finished HTTP/1.1 GET https://mybackend.com/api/saas/tenants/3a114414-b352-6751-1023-c40755a4a1bb/image - 500 null application/json; charset=utf-8 194.2802ms 20/06/2024 20:03:07 [18:03:07 INF] Request starting HTTP/1.1 GET http://mybackend.com/connect/authorize?response_type=code&client_id=Myapp_App&state=MFFzdn5-NENLRkFKTlZOLlZuRDhNVFdiUC4zVmRlMC1UX3RxV00zWkVvSk05%3B%252Ffree-inquiry&redirect_uri=https%3A%2F%2Fmyfrontend.com&scope=openid%20offline_access%20Myapp&code_challenge=0bxO4MWionxUKMExy-9tJkcC-sN8WJhDjQts2c6LHbo&code_challenge_method=S256&nonce=MFFzdn5-NENLRkFKTlZOLlZuRDhNVFdiUC4zVmRlMC1UX3RxV00zWkVvSk05&culture=en&ui-culture=en&returnUrl=%2Ffree-inquiry - null null 20/06/2024 20:03:07 [18:03:07 INF] The request URI matched a server endpoint: Authorization. 20/06/2024 20:03:07 [18:03:07 INF] The authorization request was successfully extracted: { 20/06/2024 20:03:07 "response_type": "code", 20/06/2024 20:03:07 "client_id": "Myapp_App", 20/06/2024 20:03:07 "state": "MFFzdn5-NENLRkFKTlZOLlZuRDhNVFdiUC4zVmRlMC1UX3RxV00zWkVvSk05;%2Ffree-inquiry", 20/06/2024 20:03:07 "redirect_uri": "https://myfrontend.com", 20/06/2024 20:03:07 "scope": "openid offline_access Myapp", 20/06/2024 20:03:07 "code_challenge": "0bxO4MWionxUKMExy-9tJkcC-sN8WJhDjQts2c6LHbo", 20/06/2024 20:03:07 "code_challenge_method": "S256", 20/06/2024 20:03:07 "nonce": "MFFzdn5-NENLRkFKTlZOLlZuRDhNVFdiUC4zVmRlMC1UX3RxV00zWkVvSk05", 20/06/2024 20:03:07 "culture": "en", 20/06/2024 20:03:07 "ui-culture": "en", 20/06/2024 20:03:07 "returnUrl": "/free-inquiry" 20/06/2024 20:03:07 }. 20/06/2024 20:03:08 [18:03:08 INF] Client validation failed because 'https://myfrontend.com' was not a valid redirect_uri for Myapp_App. 20/06/2024 20:03:08 [18:03:08 INF] The authorization request was rejected because the redirect_uri was invalid: 'https://myfrontend.com'. 20/06/2024 20:03:08 [18:03:08 INF] Request finished HTTP/1.1 GET https://mybackend.com/connect/authorize?response_type=code&client_id=Myapp_App&state=MFFzdn5-NENLRkFKTlZOLlZuRDhNVFdiUC4zVmRlMC1UX3RxV00zWkVvSk05%3B%252Ffree-inquiry&redirect_uri=https%3A%2F%2Fmyfrontend.com&scope=openid%20offline_access%20Myapp&code_challenge=0bxO4MWionxUKMExy-9tJkcC-sN8WJhDjQts2c6LHbo&code_challenge_method=S256&nonce=MFFzdn5-NENLRkFKTlZOLlZuRDhNVFdiUC4zVmRlMC1UX3RxV00zWkVvSk05&culture=en&ui-culture=en&returnUrl=%2Ffree-inquiry - 302 0 null 269.0968ms 20/06/2024
FYI, I have substituted the real domains with:
mybackend.com representing our backend domain myfrontend.com in place of the actual frontend domain
-
0
because 'https://myfrontend.com' was not a valid redirect_uri for Myapp_App.
Please share your authserver(openiddict) module code.
Does the
https://myfrontend.com
in yourMyapp_App
'sredirect_uri
? -
0
Hi,
Can you please tell me which file specifically you need?
-
0
hi
The module that has the
PreConfigure<AbpOpenIddictWildcardDomainOptions>(options =>
code. -
0
public class MyappHttpApiHostModule : AbpModule { public override void PreConfigureServices(ServiceConfigurationContext context) { var hostingEnvironment = context.Services.GetHostingEnvironment(); var configuration = context.Services.GetConfiguration(); PreConfigure<OpenIddictBuilder>(builder => { builder.AddValidation(options => { options.AddAudiences("Myapp"); options.UseLocalServer(); options.UseAspNetCore(); }); }); PreConfigure<AbpOpenIddictWildcardDomainOptions>(options => { options.EnableWildcardDomainSupport = true; options.WildcardDomainsFormat.Add("https://{0}.mybackend.com/"); options.WildcardDomainsFormat.Add("https://{0}.mybackend.com/signin-oidc"); options.WildcardDomainsFormat.Add("https://{0}.mybackend.com/signout-callback-oidc"); options.WildcardDomainsFormat.Add("https://{0}.mybackend.com/authentication/login-callback"); options.WildcardDomainsFormat.Add("https://{0}.myfrontend.com/"); }); #if DEBUG PreConfigure<OpenIddictServerBuilder>(options => { options.UseAspNetCore().DisableTransportSecurityRequirement(); }); #endif if (!hostingEnvironment.IsDevelopment()) { PreConfigure<AbpOpenIddictAspNetCoreOptions>(options => { options.AddDevelopmentEncryptionAndSigningCertificate = false; }); PreConfigure<OpenIddictServerBuilder>(builder => { builder.AddSigningCertificate(GetSigningCertificate(hostingEnvironment, configuration)); builder.AddEncryptionCertificate(GetSigningCertificate(hostingEnvironment, configuration)); }); } PreConfigure<IdentityBuilder>(identityBuilder=>{identityBuilder.AddSignInManager<MyAbpSignInManager>();}); } public override void ConfigureServices(ServiceConfigurationContext context) { var configuration = context.Services.GetConfiguration(); var hostingEnvironment = context.Services.GetHostingEnvironment(); if (!Convert.ToBoolean(configuration["App:DisablePII"])) { Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true; } Configure<AbpTenantResolveOptions>(options => { options.AddDomainTenantResolver(configuration["App:Domain"]!); }); if (!Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"])) { Configure<OpenIddictServerAspNetCoreOptions>(options => { options.DisableTransportSecurityRequirement = true; }); } Configure<AbpTenantResolveOptions>(options => { options.AddDomainTenantResolver(configuration["App:TenantResolverDomain"]!); }); context.Services.AddHangfire(config => { config.UseStorage(new MySqlStorage(configuration.GetConnectionString("Default"), new MySqlStorageOptions { })); }); ConfigureAuthentication(context); ConfigureUrls(configuration); ConfigureBundles(); ConfigureConventionalControllers(); ConfigureImpersonation(context, configuration); ConfigureSwagger(context, configuration); ConfigureVirtualFileSystem(context); ConfigureCors(context, configuration); ConfigureExternalProviders(context); ConfigureHealthChecks(context); ConfigureTheme(); context.Services.AddSignalR(hubOptions => { hubOptions.MaximumParallelInvocationsPerClient = 5; }); } private void ConfigureTheme() { Configure<LeptonXThemeOptions>(options => { options.DefaultStyle = LeptonXStyleNames.System; }); } private void ConfigureAuthentication(ServiceConfigurationContext context) { context.Services.ForwardIdentityAuthenticationForBearer(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); } private void ConfigureHealthChecks(ServiceConfigurationContext context) { context.Services.AddMyappHealthChecks(); } private void ConfigureUrls(IConfiguration configuration) { Configure<AppUrlOptions>(options => { options.Applications["MVC"].RootUrl = configuration["App:SelfUrl"]; options.Applications["Angular"].RootUrl = configuration["App:AngularUrl"]; options.Applications["Angular"].Urls[AccountUrlNames.PasswordReset] = "account/reset-password"; options.Applications["Angular"].Urls[AccountUrlNames.EmailConfirmation] = "account/email-confirmation"; options.RedirectAllowedUrls.AddRange(configuration["App:RedirectAllowedUrls"]?.Split(',') ?? Array.Empty<string>()); }); } private void ConfigureBundles() { Configure<AbpBundlingOptions>(options => { options.StyleBundles.Configure( LeptonXThemeBundles.Styles.Global, bundle => { bundle.AddFiles("/global-styles.css"); } ); }); } private void ConfigureVirtualFileSystem(ServiceConfigurationContext context) { var hostingEnvironment = context.Services.GetHostingEnvironment(); if (hostingEnvironment.IsDevelopment()) { Configure<AbpVirtualFileSystemOptions>(options => { options.FileSets.ReplaceEmbeddedByPhysical<MyappDomainSharedModule>(Path.Combine(hostingEnvironment.ContentRootPath, $"..{Path.DirectorySeparatorChar}Pm.Myapp.Domain.Shared")); options.FileSets.ReplaceEmbeddedByPhysical<MyappDomainModule>(Path.Combine(hostingEnvironment.ContentRootPath, $"..{Path.DirectorySeparatorChar}Pm.Myapp.Domain")); options.FileSets.ReplaceEmbeddedByPhysical<MyappApplicationContractsModule>(Path.Combine(hostingEnvironment.ContentRootPath, $"..{Path.DirectorySeparatorChar}Pm.Myapp.Application.Contracts")); options.FileSets.ReplaceEmbeddedByPhysical<MyappApplicationModule>(Path.Combine(hostingEnvironment.ContentRootPath, $"..{Path.DirectorySeparatorChar}Pm.Myapp.Application")); }); } } private void ConfigureConventionalControllers() { Configure<AbpAspNetCoreMvcOptions>(options => { options.ConventionalControllers.Create(typeof(MyappApplicationModule).Assembly); }); } private static void ConfigureSwagger(ServiceConfigurationContext context, IConfiguration configuration) { context.Services.AddAbpSwaggerGenWithOAuth( configuration["AuthServer:Authority"]!, new Dictionary<string, string> { {"Myapp", "Myapp API"} }, options => { options.SwaggerDoc("v1", new OpenApiInfo { Title = "Myapp API", Version = "v1" }); options.DocInclusionPredicate((docName, description) => true); options.CustomSchemaIds(type => type.FullName); }); } private void ConfigureCors(ServiceConfigurationContext context, IConfiguration configuration) { context.Services.AddCors(options => { options.AddDefaultPolicy(builder => { builder .WithOrigins( configuration["App:CorsOrigins"]? .Split(",", StringSplitOptions.RemoveEmptyEntries) .Select(o => o.Trim().RemovePostFix("/")) .ToArray() ?? Array.Empty<string>() ) .WithAbpExposedHeaders() .SetIsOriginAllowedToAllowWildcardSubdomains() .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); }); } private void ConfigureExternalProviders(ServiceConfigurationContext context) { var configuration = context.Services.GetConfiguration(); context.Services.AddAuthentication() .AddJwtBearer(options => { options.Authority = configuration["AuthServer:Authority"]; options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]); options.Audience = "Myapp"; options.TokenValidationParameters.IssuerValidator = TokenWildcardIssuerValidator.IssuerValidator; options.TokenValidationParameters.ValidIssuers = new[] { configuration["App:CommonUrl"] }; }) .AddGoogle(GoogleDefaults.AuthenticationScheme, _ => { }) .WithDynamicOptions<GoogleOptions, GoogleHandler>( GoogleDefaults.AuthenticationScheme, options => { options.WithProperty(x => x.ClientId); options.WithProperty(x => x.ClientSecret, isSecret: true); } ) .AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, options => { //Personal Microsoft accounts as an example. options.AuthorizationEndpoint = "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize"; options.TokenEndpoint = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; }) .WithDynamicOptions<MicrosoftAccountOptions, MicrosoftAccountHandler>( MicrosoftAccountDefaults.AuthenticationScheme, options => { options.WithProperty(x => x.ClientId); options.WithProperty(x => x.ClientSecret, isSecret: true); } ) .AddTwitter(TwitterDefaults.AuthenticationScheme, options => options.RetrieveUserDetails = true) .WithDynamicOptions<TwitterOptions, TwitterHandler>( TwitterDefaults.AuthenticationScheme, options => { options.WithProperty(x => x.ConsumerKey); options.WithProperty(x => x.ConsumerSecret, isSecret: true); } ); } private void ConfigureImpersonation(ServiceConfigurationContext context, IConfiguration configuration) { context.Services.Configure<AbpAccountOptions>(options => { options.TenantAdminUserName = "admin"; options.ImpersonationTenantPermission = SaasHostPermissions.Tenants.Impersonation; options.ImpersonationUserPermission = IdentityPermissions.Users.Impersonation; }); } public override void OnApplicationInitialization(ApplicationInitializationContext context) { var app = context.GetApplicationBuilder(); var env = context.GetEnvironment(); var forwardOptions = new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost, RequireHeaderSymmetry = false }; forwardOptions.KnownNetworks.Clear(); forwardOptions.KnownProxies.Clear(); // ref: https://github.com/aspnet/Docs/issues/2384 app.UseForwardedHeaders(forwardOptions); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseAbpRequestLocalization(); if (!env.IsDevelopment()) { app.UseErrorPage(); } app.UseAbpSecurityHeaders(); app.UseStaticFiles(); app.UseRouting(); app.UseCors(); app.UseAuthentication(); app.UseAbpOpenIddictValidation(); if (MultiTenancyConsts.IsEnabled) { app.UseMultiTenancy(); } app.UseUnitOfWork(); app.UseAuthorization(); if (!env.IsDevelopment()) { app.Use(async (httpContext, next) => { if (httpContext.Request.Path.StartsWithSegments("/swagger") && !httpContext.User.IsInRole("admin")) { httpContext.Response.StatusCode = StatusCodes.Status403Forbidden; return; } await next.Invoke(); }); } app.UseSwagger(); app.UseAbpSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "Myapp API"); var configuration = context.ServiceProvider.GetRequiredService<IConfiguration>(); options.OAuthClientId(configuration["AuthServer:SwaggerClientId"]); }); RecurringJob.AddOrUpdate<ResetMonthlyQuestionsCountJob>("reset-monthly-questions", job => job.ExecuteAsync(new ResetMonthlyQuestionsCountArgs()), Cron.Monthly); app.UseAuditing(); app.UseAbpSerilogEnrichers(); app.UseConfiguredEndpoints(); } private X509Certificate2 GetSigningCertificate(IWebHostEnvironment hostingEnv, IConfiguration configuration) { var fileName = "authserver.pfx"; var passPhrase = "f123a4f5-678a-4b4d-a2c4-3f62b6033d4d"; var file = Path.Combine(hostingEnv.ContentRootPath, fileName); if (!File.Exists(file)) { throw new FileNotFoundException($"Signing Certificate couldn't found: {file}"); } return new X509Certificate2(file, passPhrase); } }
-
0
hi
Your module code is no problem.
The
WildcardDomainsFormat
is enough.PreConfigure<AbpOpenIddictWildcardDomainOptions>(options => { options.EnableWildcardDomainSupport = true; options.WildcardDomainsFormat.Add("https://{0}.mybackend.com/"); options.WildcardDomainsFormat.Add("https://{0}.mybackend.com/signin-oidc"); options.WildcardDomainsFormat.Add("https://{0}.mybackend.com/signout-callback-oidc"); options.WildcardDomainsFormat.Add("https://{0}.mybackend.com/authentication/login-callback"); options.WildcardDomainsFormat.Add("https://{0}.myfrontend.com/"); });
Can you set the log level to Debug(MinimumLevel.Debug()) and share the Logs.txt
public class Program { public async static Task<int> Main(string[] args) { Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning) .Enrich.FromLogContext() .WriteTo.Async(c => c.File("Logs/logs.txt")) .WriteTo.Async(c => c.Console()) .CreateLogger();
liming.ma@volosoft.com
Thanks
-
0
hi
Can you remove the
/
from the end of the URL?Change
options.WildcardDomainsFormat.Add("http://{0}.localhost:4200/");
tooptions.WildcardDomainsFormat.Add("http://{0}.localhost:4200");
[10:55:12 DBG] Checking wildcard domain for url: http://test.localhost:4200 [10:55:12 DBG] Checking wildcard domain format: http://{0}.localhost:44367/ [10:55:12 DBG] Checking wildcard domain format: http://{0}.localhost:44367/signin-oidc [10:55:12 DBG] Checking wildcard domain format: http://{0}.localhost:44367/signout-callback-oidc [10:55:12 DBG] Checking wildcard domain format: http://{0}.localhost:44367/authentication/login-callback [10:55:12 DBG] Checking wildcard domain format: http://{0}.localhost:4200/ [10:55:12 DBG] Checking wildcard domain format: http://{0}.localhost:44367/ [10:55:12 DBG] Checking wildcard domain format: http://{0}.localhost:44367/signin-oidc [10:55:12 DBG] Checking wildcard domain format: http://{0}.localhost:44367/signout-callback-oidc [10:55:12 DBG] Checking wildcard domain format: http://{0}.localhost:44367/authentication/login-callback [10:55:12 DBG] Checking wildcard domain format: http://{0}.localhost:4200/ [10:55:12 DBG] Wildcard domain not found for url: http://test.localhost:4200 [10:55:13 INF] Client validation failed because 'http://test.localhost:4200' was not a valid redirect_uri for GovernmentGpt_App.