I'm fairly new to using encryption and rsa tokens and I'm trying to get IDentityServer4 to not use the developersigning, but one of my own. Here is what I have tried so far:
var keyInfo = new RSACryptoServiceProvider().ExportParameters(true);
var rsaSecurityKey = new RsaSecurityKey(new RSAParameters
{
D = keyInfo.D,
DP = keyInfo.DP,
DQ = keyInfo.DQ,
Exponent = keyInfo.Exponent,
InverseQ = keyInfo.InverseQ,
Modulus = keyInfo.Modulus,
P = keyInfo.P,
Q = keyInfo.Q
});
services.AddIdentityServer()
.AddSigningCredential(rsaSecurityKey)
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddAspNetIdentity<User>();
However, when I run Identity Server4 and I get redirected to sign in page from another website, I get the following error:
IDX10630: The '[PII is hidden]' for signing cannot be smaller than '[PII is hidden]' bits. KeySize: '[PII is hidden]'.
Parameter name: key.KeySize
I have to admit, I've been on this all weekend, trying to figure out how to use SigningCredentials and I'm not really sure what I've done wrong above.
You can see more details in development by adding the following to Configure() in the Startup class:
if (env.IsDevelopment())
{
IdentityModelEventSource.ShowPII = true;
}
For those who are having the same problem:
The ShowPII configuration is set globally, it's a static property of IdentityModelEventSource and can be set in the Startup class, for example. Once I added it I could see that it was throwing a InvalidIssuer exception for token validation. For me it was related to how I was generating the JWT to communicate with my API (which is protected with Identity Server 4). I was generating the token over the url: http://localhost:5002(out side of docker-compose network) which is different them the url Identity Server issuer inside my API: http://<<docker-service-name>>. So, if you are using docker-compose and manage to use your Identity Server as a separated container inside the same docker-compose, be aware that your authentication should generate a token with IDENTICAL issuer that is used in your API.
Related
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();
}
};
});
I have a simple web api project, which looks like this:
[Authorize]
[Route("Get")]
public ActionResult<string> SayHello()
{
return "Hello World";
}
I am trying to test it with Postman. By following the steps here: https://kevinchalet.com/2016/07/13/creating-your-own-openid-connect-server-with-asos-testing-your-authorization-server-with-postman/
1) Send the request below and receive a token as expected:
2) Attempt to send another request with the authorization token as shown below:
Why do I get a 401 (unauthorized) error? The WWW-Authenticate response header says: Bearer error="invalid_token", error_description="The issuer is invalid". I am using .Net Core 3.1. I have commented out the sensitive information in the screenshots.
The web api works as expected when accessed from an MVC application.
Here is the startup code:
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = identityUrl; //identityurl is a config item
options.RequireHttpsMetadata = false;
options.ApiName = apiName;
});
I ran into a similar issue. I was generating my token via Postman when sending in my request and using an external IP to access my Keycloak instance running inside of my kubernetes cluster. When my service inside the cluster tried to verify the token against the authority, it failed because the internal service name (http://keycloak) it used to validated the token was different than what Postman had used to generate the token (<external-keycloak-ip).
Since this was just for testing, I set the ValidateIssuer to false.
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false
};
I'm on dotnet 5.0, adding swagger (NSwag.AspNetCore) to my AzureAD "protected" web api and got a similar error about invalid issuer:
date: Tue,16 Mar 2021 22:50:58 GMT
server: Microsoft-IIS/10.0
www-authenticate: Bearer error="invalid_token",error_description="The issuer 'https://sts.windows.net/<your-tenant-id>/' is invalid"
x-powered-by: ASP.NET
So, instead of not validating the issuer, I just added sts.windows.net to the list (important parts in the end):
// Enable JWT Bearer Authentication
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
Configuration.Bind("AzureAd", options);
// Authority will be Your AzureAd Instance and Tenant Id
options.Authority = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/v2.0";
// The valid audiences are both the Client ID(options.Audience) and api://{ClientID}
options.TokenValidationParameters.ValidAudiences = new[]
{
Configuration["AzureAd:ClientId"], $"api://{Configuration["AzureAd:ClientId"]}",
};
// Valid issuers here:
options.TokenValidationParameters.ValidIssuers = new[]
{
$"https://sts.windows.net/{Configuration["AzureAd:TenantId"]}/",
$"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/"
};
});
This solved my problems. Now, why NSwag uses sts.windows.net as token issuer, I don't know. Seems wrong. I'm using these package versions:
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.3" />
<PackageReference Include="NSwag.AspNetCore" Version="13.10.8" />
The Authority of AddIdentityServerAuthentication middleware should be the base-address of your identityserver , middleware will contact the identity server's OIDC metadata endpoint to get the public keys to validate the JWT token .
Please confirm that the Authority is the url of identity server where you issued the jwt token .
Setting ValidateIssuer = false like #nedstark179 proposes will work but it will also remove a security validation.
If you use a ASP.NET Core template with Individual Accounts (IdentityServer) and receive this error:
WWW-Authenticate: Bearer error="invalid_token", error_description="The issuer 'https://example.com' is invalid"
It is probably related to this issue:
https://github.com/dotnet/aspnetcore/issues/28880
For example a new Blazor Webassembly App with Individual Accounts and ASP.NET Core hosted from Visual Studio.
.NET 6.0 Known Issues only mentions it could happen in development but it can happen in production hosted as an Azure App Service as well.
https://github.com/dotnet/core/blob/main/release-notes/6.0/known-issues.md#spa-template-issues-with-individual-authentication-when-running-in-development
I created an issue about that here:
https://github.com/dotnet/aspnetcore/issues/42072
The example fix for development was not enough.
Started of by adding a new Application settings for the Azure App Service called IdentityServer:IssuerUri with value https://example.com/. This can of course be placed in appsettings.json as well. I then added the code below:
//Used until https://github.com/dotnet/aspnetcore/issues/42072 is fixed
if (!string.IsNullOrEmpty(settings.IdentityServer.IssuerUri))
{
builder.Services.Configure<JwtBearerOptions>(IdentityServerJwtConstants.IdentityServerJwtBearerScheme, o => o.Authority = settings.IdentityServer.IssuerUri);
}
below this code:
builder.Services.AddAuthentication()
.AddIdentityServerJwt();
I have not verified if it matters where the code is placed but AddIdentityServerJwt() calls AddPolicyScheme and .AddJwtBearer(IdentityServerJwtConstants.IdentityServerJwtBearerScheme, null, o => { });. Therefore I deemed it appropriate to set it after this code has been called.
After doing this the app still failed with the same error. I then modified AddIdentityServer like this:
builder.Services.AddIdentityServer(options =>
{
//Used until https://github.com/dotnet/aspnetcore/issues/42072 is fixed
if (!string.IsNullOrEmpty(settings.IdentityServer.IssuerUri))
{
options.IssuerUri = settings.IdentityServer.IssuerUri;
}
})
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
and then it started working for me. If you still experience a problem you could also try to set AuthenticatorIssuer like this:
builder.Services.AddDefaultIdentity<ApplicationUser>(options =>
{
options.SignIn.RequireConfirmedAccount = true;
//Used until https://github.com/dotnet/aspnetcore/issues/42072 is fixed
if (!string.IsNullOrEmpty(settings.IdentityServer.IssuerUri))
{
options.Tokens.AuthenticatorIssuer = settings.IdentityServer.IssuerUri;
}
})
.AddEntityFrameworkStores<ApplicationDbContext>();
In my case, simply adding /v2.0 to the Authority was sufficient.
Case:
services.AddAuthentication(x => {
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options => {
options.Audience = "client id here";
options.Authority = "https://login.microsoftonline.com/tenant id here/v2.0";
});
UPDATE: It looks like following code is failing:
services.AddDataProtection()
.SetApplicationName(appname)
.PersistKeysToRedis(redis, "DataProtectionKeys")
.ProtectKeysWithCertificate(LoadCert(Configuration));
It's unable to read certificate from the pfx file.
UPDATE2:
Oh my! Certificate file has been excluded by .gitignore!:)) Live&Learn. At least we lived, right!?;))
Initial question:
I have ASP.NET Core 2.1 app deployed behind AWS load balancer in Docker container.
When I'm trying to log in into app from the login page. I am getting InvalidOperationException with this justification:
No authenticationScheme was specified, and there was no
DefaultChallengeScheme found.
But when I hit same URL again, it actually moving to the proper page and works for a while, then again throws same exception with HTTP status 500
and after 2nd attempt to open same page it succeeds. Interestingly enough Chrome is not as robust as IE: if IE unable to recover after exception Chrome always return 404 on subsequent submission of the page, which produced aforementioned exception.
So I would appreciate, if somebody would be able to provide me with
ideas how to remedy the situation Obviously issue is related to the
authentication, but I could not figure out exactly what should be done.
Here is relevant exert from ConfigureServices() in Startup.cs:
string appname = "MyApp";
var redis = ConnectionMultiplexer.Connect(Configuration.GetConnectionString("RedisConnection"));
services.AddDataProtection()
.SetApplicationName(appname)
.PersistKeysToRedis(redis, "DataProtectionKeys")
.ProtectKeysWithCertificate(LoadCert(Configuration));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddAuthentication( CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
options =>
{
options.LoginPath = new PathString("/Area/Ctrl/Login");
options.LogoutPath = new PathString("/Area/Ctrl/Logout");
options.Cookie.IsEssential = true;
});
services.AddDistributedRedisCache(o =>
{
o.Configuration = Configuration.GetConnectionString("RedisConnection");
});
services.AddSession(options =>
{
options.Cookie.Name = appname;
options.IdleTimeout = TimeSpan.FromSeconds(600);
});
Here is relevant code from Configure() in Startup.cs:
app.UseForwardedHeaders();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseSession();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "areas",
template: "{area:exists}/{controller=Ctrl}/{action=Login}/{id?}"
);
});
Here is how I am setting principal in controller, which handles login:
ClaimsIdentity identity = new ClaimsIdentity(GetUserRoleClaims(dbUserData), CookieAuthenticationDefaults.AuthenticationScheme);
ClaimsPrincipal principal = new ClaimsPrincipal(identity);
if (principal == null)
throw new ApplicationException($"Could not create principal for {user?.UserName} user.");
await httpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
if (httpContext.User == null)
{
httpContext.User = principal;
}
Ok, everything is working now.:)
This is what made a difference:
If app is under load balancing all instances has to share Data Protection encryption keys(e.g. use same key ring). Hence comes the Redis and a cert. Session should also be shared. Hence comes the Redis again.
Certificate for ProtectKeysWithCertificate() call should load correctly. If it could not be loaded do not make that call at all, but that's would be really bad idea. Just figure out why it's not loading.
To avoid InvalidOperationException being thrown in custom authentication HttpContext. User should be assigned manually inside Login action.
One important thing about certificate: Data Protection module supports only certs with CAPI private keys. CNG ones are left behind.
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.
I am developing an Authentication server to deliver tokens to be used to consume our API using IdentityServer4.
I am using MongoDB as database on which I have the users allowed to get tokens, and to be more secure I am using a custom certificate to encrypt the token.
This is how my Startup.cs of my AuthenticationServer looks like:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "cert", "whatever.pfx"), "whatever");
services.AddIdentityServer().AddSigningCredential(cert)
.AddInMemoryApiResources(Config.Config.GetApiResources());
services.AddTransient<IUserRepository, UserRepository>();
services.AddTransient<IClientStore, ClientStore>();
services.AddTransient<IProfileService, UserProfileService>();
services.AddTransient<IResourceOwnerPasswordValidator, UserResourceOwnerPasswordValidator>();
services.AddTransient<IPasswordHasher<User.Model.User>, PasswordHasher<User.Model.User>>();
}
As you can see I have custom implementation of those interfaces that do the client authentication and password validation. This is working fine.
Then I am protecting another application with the tokens generated, and I have there defined that it has to use the IdentityServerAuthetication (localhost:5020 is where my AuthenticationServer is running)
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseCors("CorsPolicy");
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
Authority = "http://localhost:5020",
RequireHttpsMetadata = false,
ApiName = "MyAPI",
RoleClaimType = JwtClaimTypes.Role
});
app.UseMvc();
}
All works fine, but if I shutdown the AuthenticationServer then I get this error from the API I am protecting:
System.InvalidOperationException: IDX10803: Unable to obtain configuration from: 'http://localhost:5020/.well-known/openid-configuration'.
at Microsoft.IdentityModel.Protocols.ConfigurationManager`1.d__24.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.d__1.MoveNext()
fail: Microsoft.AspNetCore.Server.Kestrel[13]
So what it seems is the API is going to the discovery endpoint to see where is the endpoint to decrypt the token (I guess it would be userinfo_endpoint).
My point is:
It seems that the discovery endpoint is used to get information of how to use the authentication server (for me makes sense for open APIs), but in our case we are not developing and open API, so our clients would be just the ones we have agreement and we will tell them in advance the endpoints and we will most likely restrict by IP address.
Is there any way to deactivate the discovery endpoint and to setup on the API the certificate to properly decrypt the token.
Maybe I am missing the complete picture and I am saying silly things, but I would be glad to understand the concepts behind.
Thanks in advance
It is possible to have the discovery endpoint off/unavailable and still validate tokens.
You'll need to implement IConfigurationManager and pass that to a JwtBearerOptions object inside of IdentityServerAuthenticationOptions.
Here's some example code:
public class OidcConfigurationManager : IConfigurationManager<OpenIdConnectConfiguration>
{
public OidcConfigurationManager()
{
SetConfiguration();
}
private OpenIdConnectConfiguration _config;
public Task<OpenIdConnectConfiguration> GetConfigurationAsync(CancellationToken cancel)
{
return Task.FromResult<OpenIdConnectConfiguration>(_config);
}
public void RequestRefresh()
{
}
private void SetConfiguration()
{
// Build config from JSON
var configJson =
#"{""issuer"":""http://localhost/id"",""jwks_uri"":""http://localhost/id/.well-known/openid-configuration/jwks"",""authorization_endpoint"":""http://localhost/id/connect/authorize"",""token_endpoint"":""http://localhost/id/connect/token"",""userinfo_endpoint"":""http://localhost/id/connect/userinfo"",""end_session_endpoint"":""http://localhost/id/connect/endsession"",""check_session_iframe"":""http://localhost/id/connect/checksession"",""revocation_endpoint"":""http://localhost/id/connect/revocation"",""introspection_endpoint"":""http://localhost/id/connect/introspect"",""frontchannel_logout_supported"":true,""frontchannel_logout_session_supported"":true,""scopes_supported"":[""openid"",""profile"",""api1"",""offline_access""],""claims_supported"":[""sub"",""name"",""family_name"",""given_name"",""middle_name"",""nickname"",""preferred_username"",""profile"",""picture"",""website"",""gender"",""birthdate"",""zoneinfo"",""locale"",""updated_at""],""grant_types_supported"":[""authorization_code"",""client_credentials"",""refresh_token"",""implicit"",""password""],""response_types_supported"":[""code"",""token"",""id_token"",""id_token token"",""code id_token"",""code token"",""code id_token token""],""response_modes_supported"":[""form_post"",""query"",""fragment""],""token_endpoint_auth_methods_supported"":[""client_secret_basic"",""client_secret_post""],""subject_types_supported"":[""public""],""id_token_signing_alg_values_supported"":[""RS256""],""code_challenge_methods_supported"":[""plain"",""S256""]}";
_config = new OpenIdConnectConfiguration(configJson);
// Add signing keys if not present in json above
_config.SigningKeys.Add(new X509SecurityKey(cert));
}
}
Now pass that config object to some JwtBearerOptions inside of your IdentityServerAuthenticationOptions (a bit annoying, but it's the only way I know of)
var identityServerOptions = new IdentityServerAuthenticationOptions
{
Authority = "http://localhost:5020",
RequireHttpsMetadata = false,
ApiName = "MyAPI",
RoleClaimType = JwtClaimTypes.Role,
};
var jwtBearerOptions = new JwtBearerOptions() {ConfigurationManager = new OidcConfigurationManager()};
var combinedOptions = CombinedAuthenticationOptions.FromIdentityServerAuthenticationOptions(identityServerOptions);
combinedOptions.JwtBearerOptions = jwtBearerOptions;
app.UseIdentityServerAuthentication(combinedOptions);
Now your API will be able to receive tokens and validate the signatures even if the OIDC discovery endpoint is off.
The API validation middleware downloads a copy of the discovery document at startup - and then (at least by default) - every 24h.
It might re-trigger download if signature validation fails (to accommodate for out of schedule key rollovers).
You can define all configuration values statically - but you are losing all the benefits of dynamic configuration updates.
If your discovery endpoint is not available, then probably the whole token service is not functional which is probably a bigger problem.
The IdentityServer needs the public key of your X509 certificate to validate the access_token. It is using the discovery endpoint to get that public key, and is refreshing the saved public key every now and then (because the public key could change).
If the IdentityServer is not accessible, your API can't guarantee that the access_token is valid. You can increase the caching of the results of the access_token validation requests.
I'm not entirely sure with IdentityServer4.AccessTokenValidation, but with IdentityServer3.AccessTokenValidation, you can set the ValidationMode to Local only, so it just downloads the public key once.