'Single Tenant Application needs to support SSO from a multi-tenant provider dynamically with OpenId Owin

I'm trying to use OpenId to authenticate against a dynamic authority URL. There is an unknown number of different {n-tenant}.identityProvider.com authority URLs. So I need to be able to pass through the n-tenant that the user is accessing my application through and configure UseOpenIdConnectAuthentication dynamically as users attempt to sign in.

This means that I won't know what the Authority URL is at startup because I wouldn't know which n-tenant to register. I would only know what the Authrity URL is after a tenant attempts to access My Application's Sign In endpoint because it will have an n-tenant value in the URL.

I attempted to use the RedirectToIdentityProvider notification to reconfigure n.Options.Authority, but that didn't work. Also, omitting the Authority configuration at startup causes an exception.

The code below works correctly if I hardcode a specific n-tenant. However, I can't figure out how to dynamically configure the OpenIdConnectAuthenticationOptions to use a dynamic n-tenant value within it's authority URL.

Please note that the ClientId and ClientSecret will be the SAME for all n-tenant. Only the endpoints need to be dynamic.

I'm using ASP.NET Forms with .NET Framework 4.8

enter image description here

Startup.cs

using IdentityModel.Client;
using Microsoft.AspNet.Identity;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin;
using Microsoft.Owin.Host.SystemWeb;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http;
using System.Security.Claims;

[assembly: OwinStartup(typeof(MyApplication.Startup))]
namespace MyApplication
{
    public class Startup
    {
        private readonly string _clientId = "CLIENT_ID";
        private readonly string _clientSecret = "CLIENT_SECRET";

        private readonly string _redirectUri = "https://myapplication.com/{n-tenant}/oidc-callback";
        private readonly string _authority = "https://identityprovider.com/{n-tenant}/";
        

        public void Configuration(IAppBuilder app)
        {
            JwtSecurityTokenHandler.DefaultMapInboundClaims = false;

            ConfigureAuth(app);
        }

        public void ConfigureAuth(IAppBuilder app)
        {
            app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

            app.UseCookieAuthentication(new CookieAuthenticationOptions());


            app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions()
            {
                ClientId = _clientId,
                ClientSecret = _clientSecret,
                Authority = _authority,
                RedirectUri = _redirectUri,
                ResponseType = OpenIdConnectResponseType.Code,
                Scope = OpenIdConnectScope.OpenId,
                TokenValidationParameters = new TokenValidationParameters { NameClaimType = "sub" },
                CallbackPath = new PathString("/{n-tenant}/oidc-callback"),
                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    RedirectToIdentityProvider = async n =>
                    {
                    },
                    AuthorizationCodeReceived = async n =>
                    {
                        using (var client = new HttpClient())
                        {

                            var tokenResponse = await client.RequestAuthorizationCodeTokenAsync(new AuthorizationCodeTokenRequest
                            {
                                Address = $"{_authority}/connect/token",
                                ClientId = _clientId,
                                ClientSecret = _clientSecret,
                                Code = n.Code,
                                RedirectUri = _redirectUri
                            });

                            if (tokenResponse.IsError)
                            {
                                throw new Exception(tokenResponse.Error);
                            }

                            n.TokenEndpointResponse = new OpenIdConnectMessage(tokenResponse.Raw);
                        }

                    }
                },
                
            });
        }
}


Solution 1:[1]

It will be hard to post an answer which will exactly help you solve the issue because only way to check if it works is to reproduce you entire environment which might be impossible.

It is very hard to find any kind of documentation for those libraries but usually when I deal with any OAuth or OpenIDConnect flows I use those repositories: Katana or IdentityModel

I think your idea about using RedirectToIdentityProvider is correct. That is the place in Katana code which is being called before redirecting user to your identity server. So you can modify parameters which are used when Url is getting build. You could start your lambda like that:

RedirectToIdentityProvider = async n =>
{
   if (n.ProtocolMessage.RequestType == Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectRequestType.Authentication)
   {
      n.ProtocolMessage.Parameters["redirect_uri"] = //your whole url here

I think it is also good idea to debug this place or log somewhere what parameters you have in that context. Because I believe you should have a parameter or property similar to IssuerAddress. This should be address to your identity server - you can modify it as well.

If you will set those values correctly whole flow will unfortunately still not work. It is because CallbackPath is used in middleware handler to identify if request which comes to your application is part of OAuth flow. You can find this code here. Without that your AuthorizationCodeReceived handler will not get called.

So now the question is - do you really need separate CallbackPath. Based on your question you are saying that application will have callbacks set like "https://myapplication.com/{n-tenant}/oidc-callback" but really what is happening there is that middleware takes the code (after user logs in) and exchange it in backchannel (server to server request) for access_token. In default flow that token gets stored in browser cookie - and in your scenario it looks like all your applications share the same cookie (are hosted on the same domain). So it is really hard to tell what is your expected behaviour after user logs in.

If that separation is "must have" in your scenario then only path I can see is really to use that Katana repository code and craft your own implemenation of OpenidConnectAuthenticationHandler.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1