Good day, I am not that familiar with this topic so excuse me if it's not explained in the best way.
I am using ServiceStack as my backend API and enabled "AllowSessionIdsInHttpParams = true". I am passing the ss-id via my url. I noticed that I can copy that ss-id and use in different clients (browser).
Should the ss-id not be validated against the user agent / client and only be validate if the client is the same?
backend code
//Configure Host
SetConfig(new HostConfig
{
DebugMode = false,
AllowSessionIdsInHttpParams = false,
UseSecureCookies = true,
UseSameSiteCookies = true,
AllowNonHttpOnlyCookies = true
});
// Plugins
Plugins.Add(new CorsFeature(AppConfig.allowedOrigins, "GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD", "Content-Type, Authorization, Accept, X-ss-id", true));
frontend
var instance = axios.create({
baseURL: Vue.prototype.$AppConfig.URL_API,
withCredentials: true,
})
Settings
Both backend and frontend is HTTPS.
setting allowedOrigins is set instead of wildcard (*) which gave different issue if I used.
Error:
401 -
Should the ss-id not be validated against the user agent / client
No, cookies are used to identify a session that given it's a unique id with high entropy is the least predictable identifier sent by HTTP clients. It's not much different to other bearer tokens such as a JWT Tokens or API Keys which authenticates the Bearer as-is, i.e. without additional user agent validation.
Validating against a User-agent would just be security theatre which is common, highly predictable & spoofable, i.e. if they're able to access your Cookie from HTTP Headers they also have access to all other HTTP Headers.
You could validate it against an IP Address but that would break sessions on a network change, e.g. whenever a device roams Wifi points which is why it's typically not done.
To best secure your cookies they should be only sent over SSL as Secure and HTTP Only Cookies (i.e. defaults in ServiceStack):
SetConfig(new HostConfig {
UseSecureCookies = true,
UseHttpOnlyCookies = true,
});
For additional protection against CSRF you can enable SameSite Cookies:
SetConfig(new HostConfig {
UseSameSiteCookies = true,
});
Although as this can break cross-site functionality and 3rd Party OAuth providers like Twitter OAuth it's set to Lax by default.
This is not a direct answer to the question, but relates to the question. This is the preferred and best solution to accomplish what I needed to accomplish in the technology stack I am using.
Servicestack backend Config:
//Configure option also Host UseSameSiteCookies = true
SetConfig(new HostConfig
{
UseSecureCookies = true
});
Plugins.Add(new CorsFeature(AppConfig.allowedOrigins, // configured Origins
"GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD",
"Content-Type, Authorization, Accept",
true)); // allowed Credentials
UseSecureCookies must be true, wildcard (*) origins did not work, allowed credentials must be true.
Axios FrontEnd (config instance for Axios)
var instance = axios.create({
baseURL: Vue.prototype.$AppConfig.URL_API,
withCredentials: true,
})
Using the instance does help to eliminate bowlerplate code, withCredentials can also be set with interceptor, but this code for me is good.
Lastly and for the people like myself who is new to this, don't forget SSL (HTTPS). In IIS you can easily create self sighed certificate for intranet I am sure it's more then enough. You need SSL on both sides, your origins (frontend) and servicestack api (backend.)
Related
Let's assume you have another website that wants to display your Angular application inside an IFRAME on one of their pages. How do you configure ASP.Net Core Antiforgery to work properly?
I spent a reasonable amount of time trying to piece this together, so this is my attempt to help others trying to figure out how to get anti-forgery tokens working with ASP.Net Core 6 and Angular application being displayed inside an IFRAME on another website.
This assumes you have already configured ASP.Net Core Antiforgery to work with Angular SPA.
ASP.Net Core Antiforgery, Angular and IFRAMES
Let's assume you have another website that wants to display your Angular application inside an IFRAME on one of their pages. You will encounter a couple of issues to get this working. Let's tackle them one at a time.
Refused to display in a frame because it set 'X-Frame-Options' to 'sameorigin'.
By default, when you call service.GetAndStoreTokens(context), the Antiforgery service sets a response header called X-Frame-Options to te value SAMEORIGIN. According to MDN, "The X-Frame-Options HTTP response header can be used to indicate whether or not a browser should be allowed to render a page in a <frame>, <iframe>, <embed> or <object>. Sites can use this to avoid click-jacking attacks, by ensuring that their content is not embedded into other sites."
In order to get past this issue, you need to turn off this behavior when setting up the Antiforgery service:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAntiforgery(options =>
{
options.HeaderName = "X-XSRF-TOKEN";
// suppress setting X-Frame-Options to SAMEORIGIN
options.SuppressXFrameOptionsHeader = true;
});
Now your web page displays inside the IFRAME, but none of the POST requests work properly. That's because there are no cookies are being passed to it.
Cookies not being sent into IFRAME
According to this excellent article, "this is a relatively new problem, as up until recently cookies would be sent through cross-site requests. It all changed when the default value for the SameSite cookie attribute was changed by Google Chrome -- introducing new default behavior that prevents these cookies from going through cross-site requests."
If you watch the network traffic, you'll notice that the cookies that come down with you application entry point are marked as SameSite:Strict. This means they will only get sent if the request comes from a client directly connected to your site. They don't get sent to the IFRAME, so your Angular application is unable to read them and send it back in the HEADER of your API request.
To resolve this problem, you need to fix both of your cookies. Both of them need to be updated to SameSite:None (cookie can be sent to any site) and Secure:true (can only be delivered over HTTPS), which will allow the cookie to flow into the IFRAME.
First, update the Antiforgery service to fix the properties on the default cookie:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAntiforgery(options =>
{
options.HeaderName = "X-XSRF-TOKEN";
// suppress setting X-Frame-Options to SAMEORIGIN
options.SuppressXFrameOptionsHeader = true;
// allow cookie to be sent to IFRAME
options.Cookie.SameSite = SameSiteMode.None;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
});
Then update the cookie being sent to the Angular application:
var app = builder.Build();
app.MapControllers();
var service = app.Services.GetRequiredService<IAntiforgery>();
app.Use(async (context, next) =>
{
var path = context.Request.Path;
if (path.Equals("/default.html", StringComparison.CurrentCultureIgnoreCase))
{
// generate .AspNetCore.Antiforgery authentication cookie
var tokenSet = service.GetAndStoreTokens(context);
var token = tokenSet.RequestToken;
// duplicate the .AspNetCore.Antiforgery authentication and create a cookie called XSRF-TOKEN
if (token != null)
{
context.Response.Cookies.Append("XSRF-TOKEN", token, new CookieOptions
{
Path = "/",
HttpOnly = false,
// allow cookie to be sent to IFRAME
SameSite = SameSiteMode.None,
Secure = true
});
}
}
await next(context);
});
At this point, the application should display properly in the IFRAME and be able to interact with the Web API. I hope this helps!
I am using .net 5, Identity Web Ui to access Microsoft Graph. Where can I configure my Redirect URI?
I need to specify the full Uri, since the generated one from callbackUri is incorrect due to being behind a Load Balancer with SSL offload.
Here is my current ConfigureServices section
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
I was facing a similar problem with a WebApp exposed only behind a front door, the WebApp had to call a custom downstream WebApi.
My service configuration that worked on my localhost dev machine:
// AzureAdB2C
services
.AddMicrosoftIdentityWebAppAuthentication(
Configuration,
"AzureAdB2C", subscribeToOpenIdConnectMiddlewareDiagnosticsEvents: true)
.EnableTokenAcquisitionToCallDownstreamApi(p =>
{
p.RedirectUri = redUri; // NOT WORKING, WHY?
p.EnablePiiLogging = true;
},
[... an array with my needed scopes]
)
.AddInMemoryTokenCaches();
I tried the AddDownstreamWebApi but did not manage to make it work so I just fetched the needed token with ITokenAcquisition and added it to an HttpClient to make my request.
Then I needed AzureAd/B2C login redirect to the uri with the front door url:
https://example.org/signin-oidc and things broke. I solved it like this:
First of all you have to add this url to your App registration in the azure portal, very important is case sensitive it cares about trailing slashes and I suspect having many urls that point to the very same controller and the order of these have some impact, I just removed everything and kept the bare minimum.
Then in the configure services method:
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.SaveTokens = true; // this saves the token for the downstream api
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async ctxt =>
{
// Invoked before redirecting to the identity provider to authenticate. This can be used to set ProtocolMessage.State
// that will be persisted through the authentication process. The ProtocolMessage can also be used to add or customize
// parameters sent to the identity provider.
ctxt.ProtocolMessage.RedirectUri = "https://example.org/signin-oidc";
await Task.Yield();
}
};
});
With that the redirect worked, but I entered a loop between the protected page and the AzureB2C login.
After a succesful login and a correct redirect to the signin-oidc controller (created by the Identity.Web package) I was correctly redirected again to the page that started all this authorization thing, but there it did not find the authorization. So I added/modded also this:
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
options.Secure = CookieSecurePolicy.Always;
});
With this the authorization worked, but I was not able to get the token to call the downstream API, before this redirect thing ITokenAcquisition worked, but now when trying to get the token it throws an exception.
So in my controller/service to get the token I modified and used:
var accessToken = await _contextAccessor.HttpContext
.GetTokenAsync(OpenIdConnectDefaults.AuthenticationScheme, "access_token");
So now with the token I add it to my HttpRequestMessage like this:
request.Headers.Add("Authorization", $"Bearer {accessToken}");
I lived on StackOverflow and microsoft docs for 3 days, I am not sure this is all "recommended" but this worked for me.
I had the same problem running an asp.net application under Google Cloud Run, which terminates the TLS connection. I was getting the error:
AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application.
Using fiddler, I examined the request to login.microsoftonline.com and found that the query parameter redirect_uri exactly matched the url I'd configured in the application in Azure except that it started http rather than https.
I initially tried the other answers involving handling the OpenIdConnectEvents event and updating the redirect uri. This fixed the redirect_url parameter in the call to login.microsoftonline.com and it then worked until I added in the graph api. Then I found my site's signin-oidc page would give its own error about the redirect uri not matching. This would then cause it to go into a loop between my site and login.microsoftonline.com repeatedly trying to authenticate until eventually I'd get a login failure.
On further research ASP.net provides middleware to properly handle this scenario. Your SSL load balancer should add the standard header X-Forwarded-Proto with value HTTPS to the request. It should also send the X-Forwarded-For header with the originating IP address which could be useful for debugging, geoip etc.
In your ASP.net application, to configure the middleware:
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
Then enable the middleware:
app.UseForwardedHeaders();
Importantly, you must include this before the calls to app.UseAuthentication/app.UseAuthorization that depends on it.
Source: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-5.0
If your load balancer doesn't add the X-Forwarded-Proto header and can't be configured to do so then the document above outlines other options.
I was facing with similar issue for 3 days. The below code helped me to get out of the issue.
string[] initialScopes = Configuration.GetValue<string>("CallApi:ScopeForAccessToken")?.Split(' ');
services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAd")
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddInMemoryTokenCaches();
services.AddControllers();
services.AddRazorPages().AddMvcOptions(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser().Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.SaveTokens = true; // this saves the token for the downstream api
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async ctxt =>
{
ctxt.ProtocolMessage.RedirectUri = "https://example.org/signin-oidc";
await Task.Yield();
}
};
});
Alright, I am stumped! I have been trying to solve this for hours now with no luck. I am following this guide to use JWT for auth in a Dot Net Core 3.1 / React.js (typescript) project I am working on to learn the whole setup. I am working using cross site requests. My React server is communicating on https://localhost:3000 (dev using Visual Studio Code), and my API / back end, API server is running on https://localhost:44309 running in Visual Studio.
I am trying to send a refresh token back to the client and the guide states this needs to be in a HTTP Only cookie to mitigate XSS. No matter what I try, I cannot get the browser to execute the ‘set-cookie’ at the client side so I can see it in Google Chrome's Dev Tools > Application > Cookies. This is for any cookie that I set in the response at all. If I use Google Chrome’s Developer Tools panel, in the network response I can see the ‘set-cookie’ headers are there, but they never show in ‘Application > Cookies > LocalHost’. The response I am sending sends the payloads and that can be used / read with no issue. It just will not set cookies!
The setup works fine when I use the same server for client and server application parts (just run it in IIS in the standard Visual Studio setup); any / all cookies set with no problems, so I am guessing I am working with a cross-site issue. I just do not know how to fix it.
My code:
//setting up cors
services.AddCors(options =>
{
options.AddPolicy("CORSAllowLocalHost3000",
builder =>
builder.WithOrigins("https://localhost:3000")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
);
});
//using cors
app.UseCors("CORSAllowLocalHost3000");
//setting up auth
services
.AddDefaultIdentity<ApplicationUser>
(
options =>
{
options.SignIn.RequireConfirmedAccount = false;
options.Password.RequiredLength = 6;
}
)
.AddEntityFrameworkStores<IdentityApplicationContext>();
services
.AddAuthentication(opts =>
{
opts.DefaultAuthenticateScheme = "JwtBearer";
opts.DefaultScheme = "JwtBearer";
opts.DefaultChallengeScheme = "JwtBearer";
})
.AddJwtBearer("JwtBearer", opts =>
{
opts.SaveToken = true;
opts.TokenValidationParameters = tokenValParams;
});
//tokenValParams to validate the JWT
var tokenValParams = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key:
Encoding.ASCII.GetBytes(configuration.GetSection("Authentication").GetSection("JwtBearer").GetSection("SecurityKey").Value)),
ValidateIssuer = false,
ValidateAudience = false,
RequireExpirationTime = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
//for API only dev env, start as API only service - no browser - client app runs
app.UseStaticFiles();
if (!env.IsEnvironment("APIOnlyDevelopment"))
app.UseSpaStaticFiles();
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
//spa.UseProxyToSpaDevelopmentServer("https://localhost:3000");
});
//Test Cookie generated in client before async Ok() result is returned in controller
HttpContext.Response.Cookies.Append("JwtRefreshTokenn", resultOfSignin.RefreshToken, new CookieOptions() { Secure = true, HttpOnly = true, SameSite = SameSiteMode.None});
Response.Cookies.Append("JwtRefreshTokenn2", resultOfSignin.RefreshToken, new CookieOptions() { HttpOnly = true, Secure = true, SameSite = SameSiteMode.None});
Response.Cookies.Append("JwtRefreshTokenn3", resultOfSignin.RefreshToken, new CookieOptions() { });
Further information:
I have changed the ‘App URL’ in Visual Studio to that of the ‘Enable SSL’ URL as I was getting a CORS issue with a redirect that was occurring.
I am running the server using the inbuilt ‘HTTPS’ setup, and the client app using npm’s https setup
(including sorting the cert error out as with this post).
I have tried all combinations of cookie options, including adding domains / paths (and all variations of same-site attribute)
I have tried different things in the CORS policy (e.g. omitting .AllowCredentials)
I have tried using http rather than https
Firefox is still having CORS issues with the requests that Google Chrome is not
The problem is mirrored in MS Edge
All running in Windows 10
I am relatively new to this, so please let me know if I have missed anything out.
Any helps is greatly appreciated. Many thanks, Paul
After a good few hours invested in this, it was a simple fix that was required on the client. The setup above is / was good and working, with a particular mention to this:
services.AddCors(options =>
{
options.AddPolicy("CORSAllowLocalHost3000",
builder =>
builder.WithOrigins("https://localhost:3000")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials() // <<< this is required for cookies to be set on the client - sets the 'Access-Control-Allow-Credentials' to true
);
});
As the client is using axios to make API calls from within React, a global default was needed to be set to match this / work with this header. So at least in one place in the program where axios is imported, the default is set as so:
import axios from 'axios';
axios.defaults.withCredentials = true;
I had a custom axios creator file setup so that I could use e.g. interceptors etc, which is where I put this code. Once these two things were added & aligned, the cookies were being set.
Hope it helps.
I see the problem was with my front end. I was using the fetch api and there I needed to add credentials: "include" then the cookie was saved.
I am struggling with basic setup of the Identity Server 4 with Net Core 3.0 and React (but this is almost irrelevant).
I have generated new app by dotnet new react -au Individual, updated dependencies etc, Created config basically copied from the demo server with the following:
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
// JavaScript Client
new Client
{
Enabled = true,
ClientId = "spa",
ClientName = "SPA (Code + PKCE)",
RequireClientSecret = false,
RequireConsent = false,
RedirectUris = { "https://notused" },
PostLogoutRedirectUris = { "https://notused" },
AllowedGrantTypes = GrantTypes.Code,
AllowedScopes = { "openid", "profile", "email", "api" },
AllowOfflineAccess = true,
RefreshTokenUsage = TokenUsage.ReUse
},
};
}
In my startup:
services.AddDefaultIdentity<ApplicationUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer(o =>
{
o.UserInteraction.ErrorUrl = "/myErrorsHandler";
o.Events.RaiseErrorEvents = true;
o.Events.RaiseFailureEvents = true;
o.Events.RaiseInformationEvents = true;
o.Events.RaiseSuccessEvents = true;
})
.AddInMemoryApiResources(Config.GetApis())
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>()
.AddInMemoryClients(Config.GetClients()) ;
Then I am trying in Postman:
and always getting:
{"displayMode":null,"uiLocales":null,"error":"unauthorized_client","errorDescription":"Unknown client or client not enabled","requestId":"0HLPL86NBMDRG:00000001","redirectUri":null,"responseMode":null,"clientId":"spa"}
I really don't understand why this is not working.
The same client on demo server with the same in Postman dialog works without any issues.
UPDATE:
I found this docs: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-api-authorization?view=aspnetcore-3.0#application-profiles
but I am still not able to get it working.
It recognizes the client, but despite the config (SPA, IdentityServerSPA) throwing:
{"displayMode":null,"uiLocales":null,"error":"invalid_request","errorDescription":"code challenge required","requestId":"0HLPL8VD22382:00000001","redirectUri":"http://localhost:5000/authentication/login-callback?error=invalid_request&error_description=code%20challenge%20required#_=_","responseMode":"query","clientId":"spa"}
UPDATE 2:
It is "working" with client defined in configuration JSON but only with predefined templates as per doc, but it is impossible (or possibility is not documented) to disable PKCE to make it work e.g. with Postman etc.
You're not defining the client_secret. Based on the code you've provided on the client's configuration you did not setup a client secret, so If no client secret is specified, there's no direct way for your client to prove its authenticity to your Authority (IDserver). This is when PKCE comes in handy, at least you can guarantee that same system is doing both requests.
I see you're asking to disable PKCE, that should not be possible (I'm not sure if it can be done but you definitely shouldn't do that) because you're using code authentication grant for an SPA. (which is the current recommended way of doing things)
As an SPA is a non-confidential client (uncapable of keeping a secret secure) this means that any other application could use your client_id spa to make requests to the token endpoint. To prevent this we combine two things:
Redirect URI: this enforces the response code token to be redirected to a previously known address which should be your client (unless using hosts file to suplant your site)
PKCE: a mechanism that aims to guarantee that both /authorize and /token requests come from the same client, so even if someone manages to intercept the code, he/she should not be able to use it in exchange for a token, because not knowing the original secret used in PKCE.
I struggled with the unauthorized_client error for the Resource Owner Password Validation flow in IdentityServer 4 because the grant type password was missing in [dbo].[ClientGrantTypes] for the associated client_id.
I had to insert a new line into the table to fix this error.
INSERT INTO [dbo].[ClientGrantTypes] (
[GrantType]
,[ClientId])
VALUES ('password', X) --where X is value of [dbo].[Clients].[Id] of used client
Is there any way to tell IdentityServer4's authentication system to allow multiple issuers for the tokens?
I have an application that is using Identity Server to issue bearer tokens, and as long as the front end and the back end use the same URL to get tokens from authentication works fine.
However, I now have a need to have the same site accessed through multiple CNAMEs, meaning that the client will request tokens from two different URLs.
The error that is sent to the logs is:
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware[7]
Bearer was not authenticated. Failure message: IDX10205: Issuer validation failed. Issuer: 'http://domainb.com'. Did not match: validationParameters.ValidIssuer: 'http://domaina.com' or validationParameters.ValidIssuers: 'null'.
The presence of a ValidIssuers collection seems to indicate that you can set multiple places from which the API will accept tokens, but I cannot find anything like that exposed in options exposed by UseIdentityServerAuthentication.
I am aware of the Authority option, but that only allows me to set a single valid authority.
Is there are any way of setting multiple valid issuers, or setting it to use something other than the hostname as the issuer id?
UPDATE
My identity server configuration on the server side looks like this:
services.AddIdentityServer(options => {
options.IssuerUri = "http://authserver"; })
.AddAspNetIdentity<ApplicationUser>();
this is from the auth server side of things.
On the client API, the UseIdentityServerAuthentication call looks like this:
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions()
{
Authority = AppSettingsConfigurationRoot["Authentication:AuthorityEndpoint"],
RequireHttpsMetadata = false,
ApiName = "rqapi",
AutomaticAuthenticate = true,
ClaimsIssuer = "http://localhost:5001"
});
The address in the {{AppSettingsConfigurationROot["Authentication:AuthorityEndpoint"] is usually set at the public DNS name of the server so that the token issuer as seen by AngularJS matches the URL of the IdentityServer from the point of view of the C# API.
As Original Poster wrote in a comment, the (now, 2020, deprecated) IdentityServer4.AccessTokenValidation package doesn't expose the right options. To read more about the recent deprecation check this blogpost, but if you still are using it, here's how I solved this issue.
The AddIdentityServerAuthentication(...) extension method is a wrapper (the code is super readable!) to combine two authentication schemes:
JwtBearer
OAuth2Introspection
It uses its own configuration class, and simply doesn't expose all the JwtBearer options (possibly just an omission, possibly because some options are not valid for both schemes.
If -like me- you only need JwtBearer you might get away with simply using just that, and using the ValidIssuers array. So:
services.AddAuthentication("Bearer")
.AddJwtBearer(options =>
{
options.Authority = "https://example.org";
options.Audience = "foo-api"; // options.ApiName in the IDS4 variant
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuers = new[]
{
"https://example.org", // first issuer
"https://example.com", // some other issuer
},
NameClaimType = "name", // To mimick IDS4's variant
RoleClaimType = "role", // To mimick IDS4's variant
};
});
As far as I understand, this will use example.org as the Authority and get the openid-configuration and so forth from that domain. But any JWT token offered to this API would be accepted as long as one of the ValidIssuers is the iss (issuer claim) in the token.