MVC Application that routes to different (fusion auth) tenants
-
I am using a MVC app (.net Core and .net Framework MVC specifically) that needs to route to different tenants depending on the url they go to.
For example:
User A goes to www.mysite.com and I want them to go to Tenant1's login page.
User B goes to www.clientname.mysite.com and I want them to go to Tenant2's login page.
Similary,
User C goes to www.anotherclient.mysite.com and I want them to go to Tenant3's login page.The issue I am running into, is that my OpenIdConnect Options is defined at the build and not per request.
My OpenIdConnect Options look like the following:private const string clientSecret = "mySecretKey"; private const string clientId = "myCleintIdGuid"; public static OpenIdConnectAuthenticationOptions MakeOptions() { return new OpenIdConnectAuthenticationOptions { AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType, SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType, ResponseType = OpenIdConnectResponseType.Code, CallbackPath = new PathString("/oauth/callback"), // this part needs to become dynamic RedirectUri = "https://clientname.mysite.com/oauth/callback", Scope = OpenIdConnectScope.OpenId, RequireHttpsMetadata = false, ClientId = clientId, Authority = "http://localhost:9011", ClientSecret = FusionAuthHelper.clientSecret, SaveTokens = true, RedeemCode = true, Notifications = new OpenIdConnectAuthenticationNotifications { RedirectToIdentityProvider = OnRedirectToIdentityProvider(), SecurityTokenValidated = OnSecurityTokenValidated() } }; }
Now, this works great for User B going to www.clientname.mysite.com (from my example above), but doesn't work for User A or User C
My Fusion Auth Looks like the following:
There might be other (FusionAuth)Applications for each tenant. But that can be a separate problem to solve if need be.
How can I do this? Or, at least, what can I look up to figure out how to best solve this issue?
-
The issue I am running into, is that my OpenIdConnect Options is defined at the build and not per request.
I'm not super familiar with this framework, but could you build those options at the request?
This SO post discusses some options (I think?): https://stackoverflow.com/questions/50488987/dynamically-set-owin-redirect-uri
-
But but that doesn't have access to the request.. or at least not that I saw/could figure out.
Instead I did something like this:
Startup.cs
// app is Owin.IAppBuilder app.Use<MyCustomAuthMiddleware>(app, FusionAuthHelper.MakeOptions());
MyCustomAuthMiddleware
public class MyCustomAuthMiddleware : OpenIdConnectAuthenticationMiddleware { public MyCustomAuthMiddleware(OwinMiddleware next, IAppBuilder app, OpenIdConnectAuthenticationOptions options) : base(next, app, options) { } public override Task Invoke(IOwinContext context) { // can do something like this if you want a service of something // var session = DependencyResolver.Current.GetService<ISession>(); if (context.Request.Host.Value.Contains("clientname")) { Options.ClientId = "clientNames_clientId"; Options.RedirectUri = "clientNames_RedirectUri"; Options.ClientSecret = "clientNames_ClientSecret"; Options.TokenValidationParameters.ValidAudience = "clientNames_clientId"; Options.ConfigurationManager = new DynamicConfigurationManager("clientNames_clientId", "clientNames_tenantId"); } else { Options.ClientId = "mySite_clientId"; Options.RedirectUri = "mySite_redirectUri"; Options.ClientSecret = "mySite_clientSecret"; Options.TokenValidationParameters.ValidAudience = "mySite_clientId"; Options.ConfigurationManager = new DynamicConfigurationManager("mySite_clientId", "mySite_tenantId"); } // could continue this pattern for "anotherClient" return base.Invoke(context); } }
FusionAuthHelper
public static class FusionAuthHelper { public static OpenIdConnectAuthenticationOptions MakeOptions() { return new OpenIdConnectAuthenticationOptions { AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType, SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType, ResponseType = OpenIdConnectResponseType.Code, CallbackPath = new PathString("/oauth/callback"), Scope = OpenIdConnectScope.OpenId, RequireHttpsMetadata = false, Authority = "http://localhost:9011", SaveTokens = true, RedeemCode = true, Notifications = new OpenIdConnectAuthenticationNotifications { SecurityTokenValidated = OnSecurityTokenValidated } }; } private static Task OnSecurityTokenValidated(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification) { var userId = new Guid(notification.AuthenticationTicket.Identity.GetUserId()); var userRepository = DependencyResolver.Current.GetService<IUserRepository>(); var user = userRepository.Query().Where(u => u.IsActive) .Where(u => u.Guid == userId) .Select(u => new { //stuff }) .SingleOrDefault(); if (user == null) { //do soemthing if the user isn't found } else { notification.AuthenticationTicket.Identity.AddClaim(new Claim("MyCoolClaim", user.Soemthing.ToString())); } return Task.CompletedTask; } }
DynamicConfigurationManager
public class DynamicConfigurationManager : IConfigurationManager<OpenIdConnectConfiguration> { private string clientId; private string tenantId; public DynamicConfigurationManager(string clientId, string tenantId) { this.clientId = clientId; this.tenantId = tenantId; } public async Task<OpenIdConnectConfiguration> GetConfigurationAsync(CancellationToken cancel) { var authority = "http://localhost:9011"; var stsDiscoveryEndpoint = string.Format("{0}/.well-known/openid-configuration/{1}", authority, this.tenantId); ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint , new OpenIdConnectConfigurationRetriever() , new HttpDocumentRetriever() { RequireHttps = false }); var config = await configManager.GetConfigurationAsync(cancel); config.EndSessionEndpoint = config.EndSessionEndpoint + "?client_id=" + this.clientId; return config; } public void RequestRefresh() { } }
here are my usings in case you wonder what packages I was using
using Microsoft.AspNet.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.Owin; using Microsoft.Owin.Security.Cookies; using Microsoft.Owin.Security.Notifications; using Microsoft.Owin.Security.OpenIdConnect; using Owin; using System; using System.Linq; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using System.Web; using System.Web.Mvc;
I'm not sure if this will create 1, or 2 options instances in the middleware.. So that might be an issue. I haven't tested it enough. Might just need to put all the FusionAuthHelper options into the middleware class.
Also, I think this will only work for MVC .Net Framework. Still need to come up with solution for .NET Core
-
Not sure if this helps, as we don't currently use different tenants at this point in time, but we do for sure enforce sending the tenant id to each call:
When you setup the OpenIdConnectOptions ---
private const string TenantIdParameterName = "tenantId"; ... options.Events.OnRedirectToIdentityProvider = context => { /* Fusion auth has the option for multiple tenants - when multiple tenants enabled, we have to ensure we hit the right one for user auth. */ context.ProtocolMessage.SetParameter(TenantIdParameterName, authSettings.TenantId.ToString()); } options.Events.OnRedirectToIdentityProviderForSignOut = context => { context.ProtocolMessage.ClientId = authSettings.ClientId.ToString(); context.ProtocolMessage.SetParameter(TenantIdParameterName, authSettings.TenantId.ToString()); return Task.CompletedTask; };
Not sure if that helps you - you will have to look at the current HttpContext to decide what you want to do.