IdentityServer4 ValidIssuers - c#

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.

Related

Load roles from database based in JWT's userId

I'm trying to implement some database fetching in my JWT authentication, so that I can dinamically fetch the roles and other info from the database, I would ideally only want to store the userId in the JWT payload.
Doing that, I can do stuff like store the date where the password was last changed, and refuse the token in case it was issued before that.
The problem is that I have no idea where on the chain I would do such thing, I'm just getting started with .NET/ASP.NET
I have this on my ConfigureServices currently:
var jwtKey = Encoding.ASCII.GetBytes(_configuration["Jwt:Secret"]);
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(jwtKey),
ValidateIssuer = false,
ValidateAudience = false,
};
});
And this on my Configure:
app.UseAuthentication();
app.UseAuthorization();
With that, I can access some user info inside a controller's action with User.Identity.Name for example, how would I do that after loading the model from the DB?
REQUIREMENTS
These are usually the main things you will care about:
APIs must have access to domain specific claims so that they can authorize correctly
Tokens returned to internet clients are kept confidential
OPTION 1
The optimal way to meet the requirements is to issue opaque tokens to internet clients, then introspect them before they reach the API, as described in the Phantom Token Approach.
An Authorization Server can then reach out to your domain specific data at the time of token issuance, to include custom claims in access tokens.
OPTION 2
In the API code there are two stages involved in integrating custom claims, and the second of these can be customized:
Verify the JWT access token
Form a ClaimsPrincipal
If your Authorization Server doesn't support opaque access tokens, another option that will work is a Custom Authentication Handler. It adds some complexity to your API though. See this blog post of mine and this code.

.Net Core API JWT Token Validation

Implemented the JWT Bearer Token validation in .Net Core WEB API as mentioned below:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
opt.Audience = Configuration["AAD:ResourceId"];
opt.Authority = $"{Configuration["AAD:Instance"]}{Configuration["AAD:TenantId"]}";
});
Doubt here is the above mentioned code will validate only the audience and authority ? or it will validate all the parameters like expiration and signature etc. ?
Do we need to validate the signature explicitly to check the payload has been tampered ?
I think you're looking for this:
https://zhiliaxu.github.io/how-do-aspnet-core-services-validate-jwt-signature-signed-by-aad.html
Here zhiliaxu explains in details how and what is actually validated when using .AddJwtBearer() and their conclusions are:
Now it is clear that
JWT signature is validated without providing any key or certification
in our service’s source code.
JWT signing key is retrieved from the well-known URL https://login.microsoftonline.com/common/discovery/keys, based on
JwtBearerOptions.Authority property.
The signing key is cached in the JwtBearerHandler singleton instance, and so our ASP.NET Core service only needs to retrieve it
once throughout its lifecycle.
Also based on this article we can take a look at the ValidateToken() documentation on MSDN: https://learn.microsoft.com/en-us/dotnet/api/system.identitymodel.tokens.jwt.jwtsecuritytokenhandler.validatetoken?view=azure-dotnet Where you can find the different exceptions the method throws:
SecurityTokenDecryptionFailedException: token was a JWE was not able to be decrypted.
SecurityTokenEncryptionKeyNotFoundException: token 'kid' header claim is not null AND decryption fails.
SecurityTokenException: token 'enc' header claim is null or empty.
SecurityTokenExpiredException: token 'exp' claim is < DateTime.UtcNow.
SecurityTokenInvalidAudienceException: token 'aud' claim did not match either ValidAudience or one of ValidAudiences.
SecurityTokenInvalidLifetimeException: token 'nbf' claim is > 'exp' claim.
SecurityTokenInvalidSignatureException: token.signature is not properly formatted.
SecurityTokenNoExpirationException: TokenReplayCache is not null and expirationTime.HasValue is false. When a TokenReplayCache is set, tokens require an expiration time.
SecurityTokenNotYetValidException: token 'nbf' claim is > DateTime.UtcNow.
SecurityTokenReplayAddFailedException: token could not be added to the TokenReplayCache.
SecurityTokenReplayDetectedException: token is found in the cache.
It will validate issuer, audience and lifetime by default. There's a bunch of properties in TokenValidationParameters. If you create a new instance of that class, you'll see which fields are set to true/false. Or, you could add the following to your code, breakpoint and investigate yourself.
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{ ...
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false, ...
}; ..
} ...
NB authority and issuer are pretty much the same concept. Also, mind the difference between ValidIssuer and ValidateIssuer.

ServiceStack Session ID in URL

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.)

ASP.NET Core 3+ JWT authentication

Been spending a few days lately getting into how to protect a ASP.NET Core Web API/Web App with JWT authentication and/or OIDC.
I have so far been unable to find proper documentation to the two(?) different middleware extensions (.AddOpenIdConnect and .AddJwtBearer). From googling and experimenting I have arrived at the assumption that the former supports the full OIDC dance (validation, redirecting to authority etc., suitable for a web app) whereas the latter is used for JWT validation only (more suitable for an API).
What articles I have found are all recipes, focused on how to configure for a specific id provider, such as Azure or IdentityServer4, but I have found almost nothing that actually dives into how these component behaves and how each configurable option affects that behavior. My company uses its own id provider for authentication and issuing JWT tokens so most recipes I find only provides the occasional hint, not real understanding.
I realize documentation must exist but I haven't been able to find anything beyond the skeleton doc provided by Microsoft on learn.microsoft.com.
Would really appreciate more in-detail information for how to use these two middlewares; not specifically for a named id provider, but more generally:
Is my assumption correct, that .AddOpenIdConnect performs both JWT validation and supports the OIDC dance, automatically redirecting to the authority, token issuer etc?
Is the middleware dependent on cookie auth; i.e. can I omit the .AddCookie setup?
Would it ever make sense to use both middleware components (.AddOpenIdConnect and .AddJwtBearer)?
Please describe the behavior of each component, and how the options affect that behavior.
What are the required options that have to be specified and which ones are optional?
Is there a need to interact with the OAuth2 "dance" (via events) or are those there for debugging and response/redirect customization only?
AddOpenIdConnect is as you say responsible for the oauth dance, to authenticate user and to create the user session. It handle everything internally, so you dont need to involve the events unless you need to customize it.
You typcically use AddOpenIdConnect with AddCookie, so that AddCookie is responsble for the user session cookie. AddOpenIdConnect is only used for the challenge part of the user login flow.
AddJwtBearer is only for for API/services that receives and authenticate tokens, it will only do valdidation of the token and create a ClaimsPrincipal user based on the token.
you can use both in the same service, but I recommend that you don't because it can get really complex to figure out what is going on. Keep it simple and follow the single responsibility principle and you are good to go. (ie, put them in different services)
A typical AddJwtBearer setup can look like this:
.AddJwtBearer(opt =>
{
opt.Authority = "https://localhost:6001";
opt.Audience = "paymentapi";
opt.TokenValidationParameters.RoleClaimType = "roles";
opt.TokenValidationParameters.NameClaimType = "name";
opt.TokenValidationParameters.ClockSkew = TimeSpan.FromSeconds(0);
// IdentityServer emits a typ header by default, recommended extra check
opt.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
});
A typical AddOpenIdConnect setup can look like this:
.AddOpenIdConnect(options =>
{
options.AccessDeniedPath = "/User/AccessDenied";
options.Authority = _configuration["openid:authority"];
options.ClientId = _configuration["openid:clientid"];
options.ClientSecret = "mysecret";
options.ResponseType = "code";
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("employee");
options.Scope.Add("payment");
options.Scope.Add("offline_access");
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.Prompt = "consent";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtClaimTypes.Name,
};
});

Identity Server 4 - unauthorized client

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

Categories