Nate
Nate

Reputation: 61

IdentityServer4 how to access parameters in /connect/token request

I am trying to create an IdentityServer4 solution (ASP.NET Core 3.1, Entity Framework Core) that is multi-tenant with a database-per-tenant model.

I start by passing acr_values tenant:tenantname. I then extract this from the request and dynamically get the proper connection string.

However, I eventually end up with this OpenIdConnectProtocolException: Message contains error: 'invalid_grant', error_description: 'error_description is null', error_uri: 'error_uri is null'.

It happens as it attempt to read/write to PersistedGrants. I can tell by the time I get to the /connect/token endpoint where this occurs, I lose all track of the tenant name in the request. It's no longer in a query string, body, nothing... but I also don't have an authenticated user at this point to look at a claim.

What would be a good way to access this information to properly connect to the database for that final request?

I've attached just my EntityFrameworkCore db context configuration, because that's where all the magic is happening.

            services.AddDbContext<MyAppDbContext>((serviceProvider, options) =>
            {
                // Get the standard default connection string
                string connectionString = Configuration.GetConnectionString("DefaultConnection");

                // Inspect the HTTP Context
                var httpContext = serviceProvider.GetService<IHttpContextAccessor>().HttpContext;

                // function to parse the tenant name from the acr (i.e. tenant:testtenant)
                string GetTenantNameFromRequest(HttpRequest request)
                {
                    string ParseTenantName(string acrValues)
                    {
                        return Regex.Match(acrValues, @"tenant:(?<TenantName>[^\s]*)").Groups["TenantName"]?.Value;
                    }

                    if (request == null || request.Query?.Count == 0)
                        return null;

                    // Get the possible queries for the tenant name to show up in
                    var acr = request.Query["acr_values"];
                    if (!string.IsNullOrEmpty(acr))
                        return ParseTenantName(acr);

                    var returnUrl = request.Query["returnUrl"]; // Web MVC
                    if (!string.IsNullOrEmpty(returnUrl))
                    {
                        NameValueCollection returnUrlQuery = HttpUtility.ParseQueryString(returnUrl);
                        return ParseTenantName(returnUrlQuery["acr_values"]);
                    }

                    var redirectUri = request.Query["redirect_uri"]; // OIDC Client (WinForms)
                    if (!string.IsNullOrEmpty(redirectUri))
                    {
                        NameValueCollection returnUrlQuery = HttpUtility.ParseQueryString(returnUrl);
                        return ParseTenantName(returnUrlQuery["acr_values"]);
                    }

                    //  connect/token does not include any information about the authentication request

                    return null;
                }

                string tenantName = GetTenantNameFromRequest(httpContext?.Request);
                if (!string.IsNullOrEmpty(tenantName) && string.Compare(tenantName, "localhost", true) != 0)
                {
                    // call catalog to get the tenant connection information
                    var tenantLookupService = serviceProvider.GetService<TenantLookupService>();
                    connectionString = tenantLookupService.GetTenantConnectionStringAsync(tenantName).GetAwaiter().GetResult();
                }

                // connect with the proper connection string.
                options.UseSqlServer(connectionString, o =>
                {
                    o.EnableRetryOnFailure();
                });
            });

here is the IdentityServer4 setup code, though I don't think it's relevant to this issue.

            services.AddDefaultIdentity<User>(options => options.SignIn.RequireConfirmedAccount = true)
                .AddEntityFrameworkStores<MyAppDbContext>()
                .AddDefaultTokenProviders();

            services.AddTransient<IProfileService, ProfileService>();

            var builder = services.AddIdentityServer(options =>
            {
            })
            .AddInMemoryApiResources(Config.Apis)
            .AddInMemoryClients(Config.Clients)
            .AddInMemoryIdentityResources(Config.GetIdentityResources())
            .AddAspNetIdentity<User>()
            .AddOperationalStore<MyAppDbContext>(options =>
            { 
                options.EnableTokenCleanup = true;
            })
            .AddProfileService<ProfileService>()
            .AddDeveloperSigningCredential();

Update... trying new method of accessing via cookie

            services.AddDbContext<MyAppDbContext>((serviceProvider, options) =>
            {
                string connectionString = Configuration.GetConnectionString("DefaultConnection");

                var httpContext = serviceProvider.GetService<IHttpContextAccessor>().HttpContext;

                string GetTenantNameFromRequest(HttpContext context)
                {
                    string ParseTenantName(string acrValues)
                    {
                        return Regex.Match(acrValues, @"tenant:(?<TenantName>[^\s]*)").Groups["TenantName"]?.Value;
                    }

                    var request = context?.Request;

                    if (request == null) //|| request.Query?.Count == 0)
                        return null;

                    // Get the possible queries for the tenant name to show up in
                    var acr = request.Query["acr_values"];
                    if (!string.IsNullOrEmpty(acr))
                    { 
                        string tenantName = ParseTenantName(acr);
                        if (!string.IsNullOrEmpty(tenantName))
                        {
                            CookieOptions cookieOptions = new CookieOptions();
                            cookieOptions.IsEssential = true;
                            cookieOptions.SameSite = SameSiteMode.Strict;
                            cookieOptions.Secure = true;
                            cookieOptions.Expires = DateTime.Now.AddMinutes(10);
                            context.Response.Cookies.Append("signin-tenant", tenantName, cookieOptions);
                        }

                        return tenantName;
                    }
                    else
                    {
                        string tenantName = context.Request.Cookies["signin-tenant"];
                        return tenantName;
                    }
                }

                string tenantName = GetTenantNameFromRequest(httpContext);
                if (!string.IsNullOrEmpty(tenantName) && string.Compare(tenantName, "localhost", true) != 0)
                {
                    var tenantLookupService = serviceProvider.GetService<TenantLookupService>();
                    connectionString = tenantLookupService.GetTenantConnectionStringAsync(tenantName).GetAwaiter().GetResult();
                }

                options.UseSqlServer(connectionString, o =>
                {
                    o.EnableRetryOnFailure();
                });
            });

            services.AddDefaultIdentity<User>(options => options.SignIn.RequireConfirmedAccount = true)
                .AddEntityFrameworkStores<MyAppDbContext>()
                .AddDefaultTokenProviders();

Upvotes: 1

Views: 2270

Answers (2)

MayankGaur
MayankGaur

Reputation: 993

enter image description here
Hi I have achieved this by adding below code

 string acr_values = context?.ValidatedRequest?.Raw.Get("acr_values");

Moreover if you want to inject acr_value in your client, you can inject it through it

  context.TokenEndpointRequest.Parameters.Add("acr_value", "tenantCode:xyz");

Upvotes: 2

Nate
Nate

Reputation: 61

This can't be achieved and was confirmed by leastprivilege. If you need to access tenant name through the whole flow, acr_values is not the way to go.

Upvotes: 0

Related Questions