Identity Server 4 Running Behind a load Balancer - c#

I have setup Identity Server 4 for my project using Entity Framework. I already configured the service to use a persisted grant Store and a Signed Certificate.
services.AddIdentityServer()
.AddSigningCredential(Config.GetSigningCertificate())
.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
.AddProfileService<ProfileService>()
.AddConfigurationStore(builder =>
builder.UseSqlServer(connectionString, options =>
options.MigrationsAssembly(migrationsAssembly)))
.AddOperationalStore(builder =>
builder.UseSqlServer(connectionString, options =>
options.MigrationsAssembly(migrationsAssembly)));
Here is the configuration of the service.
The problem is when I run my server behind a load balancer with for exemple 2 identic instances handling all the request, the server where the user did not logged in fail to decode the JWT token, leading to 401 unauthorized errors.
I'm assuming the sigining method of the tokens or their encription is the problem but I cannot find a way to solve this.
Here is the rest of my configuration.
The Configure:
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
Authority = url,
// Authority = "http://localhost:5000",
AllowedScopes = { "WebAPI" },
RequireHttpsMetadata = false,
AutomaticAuthenticate = true,
AutomaticChallenge = true,
});
the Client:
new Client
{
ClientId = "Angular2SPA",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, // Resource Owner Password Credential grant.
AllowAccessTokensViaBrowser = true,
RequireClientSecret = false, // This client does not need a secret to request tokens from the token endpoint.
AccessTokenLifetime = 7200, // Lifetime of access token in seconds.
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId, // For UserInfo endpoint.
IdentityServerConstants.StandardScopes.Profile,
"roles",
"WebAPI"
},
AllowOfflineAccess = true, // For refresh token.
AccessTokenType = AccessTokenType.Jwt
}
I also implemented my own IResourceOwnerPasswordValidator and IProfileService.
Any idea why is this happening?

I had a similar issue, load balancing Identity Server 4 and was able to share the keys using .AddDataProtection() in ConfigureServices of Startup.cs .
public void ConfigureServices(IServiceCollection services)
{
// Other service configurations
services.AddDataProtection();
// Additional service configurations
}
As a side note, if you go this route, consider encrypting those keys (in whichever medium you decide to use) using an extension like
.ProtectKeysWith* (there are several options)
. See https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/introduction?view=aspnetcore-2.1 for further information
HTH

Related

Web API call returns 'Unauthorized' message despite configuring JwtHelper in Angular UI

I have a Web API written in .net core 5.0 and I am using Angular 10 for the front end. For now I have kept all the APIs open, however, to move this application in production I need to validate each API call and check if a logged in user is sending these requests.
I have modified the API controller like this :
[Authorize]
[HttpGet]
public List<ReportDataValues> GetReportingData()
{
var ReportData = _context.ReportDataValues.ToList();
return ReportData;
}
Here is my relevant code for the startup.cs file which configures the JWT authentication :
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = false;
x.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(LoginKey),
ValidateIssuer = true,
ValidateLifetime = true,
ValidIssuer = "https://localhost:5001",
ValidAudience = "https://localhost:5001",
ValidateAudience = true,
};
});
}
I am using the #auth0/angular-jwt library to include authorization token in each http request. In my app.module.ts file I have declared a function :
export function tokenGetter(){
return localStorage.getItem('token');
}
Here, token is the id for the key, where the server is returning the token value for the login request. I have verified that a token gets generated and is getting stored in the localStorage
In the app.module.ts I have also included the configuration to include the tokens in each request :
imports: [
JwtModule.forRoot({
config : {
tokenGetter : tokenGetter,
allowedDomains : ["localhost:44326"],
disallowedRoutes : []
}
}),
BrowserModule,
AutocompleteLibModule,
BrowserAnimationsModule,
Based on my understanding this should be enough to include the auth headers in each http request. Here is the http request I am making for this API controller :
return this.http
.get<any[]>(GlobalConstants.baseURL + 'ReportingData')
However, I get an error saying that this request is unauthorized and I see the Network tab in the dev tools has the Bearer token inside it and it is the same token that can be seen in the Application tab in dev tools:
The console logs this error as below :
Can someone please tell me if I need to make any other configuration changes to enable this request to be authorized?

Bearer error - invalid_token - The signature key was not found

I have an Angular 7 application interfacing with a .Net Core 2.2 API back-end. This is interfacing with Azure Active Directory.
On the Angular 7 side, it is authenticating properly with AAD and I am getting a valid JWT back as verified on jwt.io.
On the .Net Core API side I created a simple test API that has [Authorize] on it.
When I call this method from Angular, after adding the Bearer token, I am getting (as seen in Chrome Debug Tools, Network tab, "Headers"):
WWW-Authenticate: Bearer error="invalid_token", error_description="The
signature key was not found"
With a HTTP/1.1 401 Unauthorized.
The simplistic test API is:
[Route("Secure")]
[Authorize]
public IActionResult Secure() => Ok("Secure works");
The Angular calling code is also as simple as I can get it:
let params : any = {
responseType: 'text',
headers: new HttpHeaders({
"Authorization": "Bearer " + token,
"Content-Type": "application/json"
})
}
this.http
.get("https://localhost:5001/api/azureauth/secure", params)
.subscribe(
data => { },
error => { console.error(error); }
);
If I remove the [Authorize] attribute and just call this as a standard GET request from Angular it works fine.
My Startup.cs contains:
services
.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureADBearer(options => this.Configuration.Bind("AzureAd", options));
The options are all properly set (such as ClientId, TenantId, etc) in the appsettings.json and options here is populating as expected.
I was facing the same issue. i was missing the authority..make sure authority and api name is correct now this code in configure services in startup file works for me:
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication( x =>
{
x.Authority = "http://localhost:5000"; //idp address
x.RequireHttpsMetadata = false;
x.ApiName = "api2"; //api name
});
I had a unique scenario, hopefully this will help someone.
I was building an API which has Windows Negotiate authentication enabled (.NET Core 5.0, running from IIS) and unit testing the API using the CustomWebApplicationFactory (see documentation for CustomWebApplicationFactory) through XUnit which does not support Negotiate authentication.
For the purposes of unit testing, I told CustomWebApplicationFactory to use a "UnitTest" environment (ASPNETCORE_ENVIRONMENT variable) and specifically coded logic into my application Startup.cs file to only add JWT authentication for the "UnitTest" environment.
I came across this error because my Startup.cs configuration did not have the signing key I used to create the token (IssuerSigningKey below).
if (_env.IsEnvironment("UnitTest"))
{
// for unit testing, use a mocked up JWT auth, so claims can be overridden
// for testing specific authentication scenarios
services.AddAuthentication()
.AddJwtBearer("UnitTestAuth", opt =>
{
opt.Audience = "api://local-unit-test";
opt.RequireHttpsMetadata = false;
opt.TokenValidationParameters = new TokenValidationParameters()
{
ClockSkew = TokenValidationParameters.DefaultClockSkew,
ValidateAudience = true,
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
ValidAudience = "api://local-unit-test",
ValidIssuer = "unit-test",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("abcdefghijklmnopqrstuvwxyz123456"))
};
});
} else {
// Negotiate configuration here...
}
Regardless of the ValidateIssuerSigningKey being true or false, I still received the "invalid_token" 401 response, same as the OP. I even tried specifying a custom IssuerSigningKeyValidator delegate to always override the result, but did not have luck with that either.
WWW-Authenticate: Bearer error="invalid_token", error_description="The signature key was not found"
When I added IssuerSigningKey to the TokenValidationParameters object (of course matching the key I used when generating the token in my unit test), everything worked as expected.
There could be two reason:
You might have missed registering service :
services.AddAuthorization(auth =>
{
auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme‌​)
.RequireAuthenticatedUser().Build());
});
Or
2. You might have missed assigning value to key "IssuerSigningKey" as shown below
validate.TokenValidationParameters = new TokenValidationParameters()
{
ValidateAudience = true,
ValidAudience = "Audience",
ValidateIssuer = true,
ValidIssuer = "http://localhost:5000",
RequireExpirationTime = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("abcdefghi12345"))
});
This resolved my problem
My problem was that I needed to set the ValidIssuer option in the AddJwtBearer TokenValidationParameters, in addition to the authority
For example:
services.AddAuthentication("Bearer")
.AddJwtBearer(options =>
{
options.Audience = "My Audience";
options.Authority = "My issuer";
options.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ValidateIssuer = true,
ValidIssuer = "Also My Issuer", //Missing line here
ValidateAudience = true
};
});
Verify the values that you send for request the jwt token (eg: grant_type, client_secret, scope, client_id, etc)
Ensuere that you are using the appropiate token. That's all!
Here is my mistake:
I was using Postman, and request a token and set it to a varibale "Var_Token1":
pm.environment.set("Var_Token1", pm.response.json().access_token);
But when I need to use the token for my final request, I selected and use the wrong token (Var_Token2):
Authorization: Bearer {{Var_Token2}}
For me, this error was coming because the URL in appsettings.json was incorrect. I fixed it and it's working fine now.
This can also happen if you are using a different SignedOutCallbackPath/SignUpSignInPolicyId policy id than which is being passed in the token as tfp/acr.
My Core API uses different services configuration (and it works :)):
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
Configuration.Bind("JwtBearer", options);
}
Are you sure you are passing an access token and not an id_token? Is the aud claim present in the token exactly the same as the clientid your API is configured with? You may want to add some events to your options to see what you are receiving and where the validation fails.
I had this issue, and it was caused by jwtOptions.Authority not being set in config.
If you are using:
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme,
And jwtOptions.Authority is set to null or "" you can get this error message.

Identity4Server many SPA clients best approach?

Hi I have inherit a system like this:
An Api and many Fronts (spas) they share a common menu with links to navigate to each others but they are different react apps, with different urls. And Azure Active directory to authenticate an the Api is protected with Bearer token.
Something like this:
Now I have authorization requirements with a custom permissions that the business people want to assign to every user, for actions that they can do or not and visibility things.
I want to use Identity4Server with the active directory as an open id provider. Then consume a provider api to get custom permission and put those permissions in the claims. Then in the Api impl policies that demand for specify roles and claims to accomplish the permissions specifications.
Something like this:
Identity4Server config:
services.AddAuthentication()
.AddOpenIdConnect("oidc", "OpenID Connect", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.SaveTokens = true;
options.RequireHttpsMetadata = false;
options.Authority = "https://login.microsoftonline.com/tenant/";
options.ClientId = "ClientId";
options.ClientSecret = "ClientSecret";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
Api:
services
.AddAuthentication(configure =>
{
configure.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
configure.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Audience = "api";
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
});
var clientsPolicy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes("Bearer")
.AddRequirements(new ClaimsAuthorizationRequirement("ClientsModule", new[] { "1" }))
.RequireRole("Admin")
.Build();
services.AddAuthorization(options =>
{
options.AddPolicy("Clients", clientsPolicy);
});
For the react apps I'm using this npm "oidc-client": "1.7.0" and a similar approach to https://medium.com/#franciscopa91/how-to-implement-oidc-authentication-with-react-context-api-and-react-router-205e13f2d49
And the Clients config is: (Provider its quite similar the only thing that change is url localhost:3001)
export const IDENTITY_CONFIG = {
authority: "http://localhost:5000",
clientId: "fronts",
redirect_uri: "http://localhost:3000/signin-oidc",
login: "http://localhost:5000/login",
automaticSilentRenew: false,
loadUserInfo: false,
silent_redirect_uri: "http://localhost:3000/silentrenew",
post_logout_redirect_uri: "http://localhost:3000/signout-callback-oidc",
audience: "fronts",
responseType: "id_token token",
grantType: "password",
scope: "openid api",
webAuthResponseType: "id_token token"
};
If the user login into clients (localhost:3000) front and then navigate to providers (localhost:3001) front it shouldn't login again. To accomplish this I configure all the fronts with the same client id, but I don't know if this is the correct way to do it. Now my config class in identity server is:
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "fronts",
ClientSecrets =
{
new Secret("secret".Sha256())
},
ClientName = "All fronts",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
RedirectUris = { "http://localhost:3000/signin-oidc", "http://localhost:3001/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:3000/signout-callback-oidc", "http://localhost:3001/signout-callback-oidc" },
AllowedCorsOrigins = { "http://localhost:3000", "http://localhost:3001" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api"
}
}
};
}
Do you think this configuration is the correct way to do it or there is a better approach?
You mentioned
many different react apps, with different urls
but in your code snippet I see only the Clients(localhost:3000).
Anyway, the protocol spec tells us to register as many clients as we need. SSO is the main responsibility of identity provider.
You just need to add RequireConsent = false; to your client def in IdSrv to avoid additional unintended user interaction.
Additionally, nowadays the recommended auth flow for spa-s is "code+pkce". You can take a look at this article in order to get detailed info for transition.

Token with multiple Scopes can only get access to single Scope/Api Resource

I have a Problem with Authenticating my Applications with IdentityServer4.
I have:
a IdentityServer4-Server to authenticate multiple Clients
(running on localhost:5000)
a Api which is mainly used for storing and retrieving data
with SignalR (running on localhost:5200)
a Web-Application to read & write Data from the Api and have
User specific interactions (running on localhost:80)
and a Desktop-Application to read Data from Api
What works:
Desktop User can use "ApiClient" to use Api with
Client-Credential-Flow.
Web User can use "WebClient" to get a Token with Implicit-Flow when
Logging in.
What doesnt work:
Web Client cant Authenticate on the Api using his Token from
Implicit-Flow, even though he has the scope for the Api.
ApiResources:
public static IEnumerable<ApiResource> Apis = new List<ApiResource>
{
new ApiResource("WebApplicationResource", ""),
new ApiResource("ApiServerResource", "")
};
Clients:
public static IEnumerable<Client> Clients = new List<Client>
{
// Web Application (SPA) Client
new Client
{
ClientId = "WebClient",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
RequireConsent = false,
RedirectUris = {
"http://localhost:3000/callback"
},
PostLogoutRedirectUris = {
"http://localhost:3000/post_logout"
},
AllowedCorsOrigins = {
"http://localhost:3000"
},
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"WebApplicationResource",
"WebApiResourceScope"
}
},
// Api (Signalr) Client
new Client
{
ClientId = "ApiClient",
AllowedGrantTypes = GrantTypes.ClientCredentials,
RequireConsent = false,
ClientSecrets = {
new Secret("SecretCredentialsValue".Sha256())
},
AllowedScopes = {
"WebApiResourceScope"
}
}
};
IS4 Server Authentication in Startup:
//in ConfigureServices
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", jwt =>
{
jwt.Authority = "http://localhost:5000";
jwt.Audience = "WebApplicationResource";
jwt.RequireHttpsMetadata = false;
});
...
//in Configure
app.UseIdentityServer();
Api Server adding IS4 Authentication in Startup:
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ApiName = "WebApiResourceScope";
});
Api Server SignalR implementation in Startup:
//in ConfigureServices
services.AddSignalR()
...
//in Configure
app.UseSignalR(routes =>
{
routes.MapHub<EventHub>("/api");
});
SignalR Hub Authentication added with:
[Authorize(AuthenticationSchemes = "Bearer")]
Here is the output when testing:
Api showing that Desktop Client can Authenticate:
The Web Client after successfully logging in, trying to authenticate on the SignalR Server which is on the Api (like the Client did on top) with its bearer token.
Console Output when Error happening:
Output of Api, showing that Bearer Token was not authenticated:
Did I misunderstood something about the scopes? Why can i add multiple scopes on Web Client but only use it on one of the declared Api Resources?
Do I have to get 2 Tokens for the Web Client? If so, what should i use to get a Token, Client Credentials isn't recommended on Web to Web communication.
Or what is it that I am not understanding.

Configuring different authorization/authentication schemes

I am implementing security on an ASP.NET Core 1.0.1 application, which is used as a Web API. I am trying to understand if and how to implement 2 different authentication schemes.
Ideally, I would like to allow authentication via Azure Active Directory or via username/password for specific back-end services that contact the application.
Is it possible to configure ASP.NET Core for such a setup where an endpoint either authenticates through Azure AD or JWT token?
I tried with something like this, but upon calling the generate token endpoint, I get a 500 with absolutely no information. Removing the Azure AD configuration makes the endpoint work perfectly:
services.AddAuthorization(configuration =>
{
configuration.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().Build());
configuration.AddPolicy("OpenIdConnect", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(OpenIdConnectDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().Build());
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = Configuration["Authentication:AzureAD:ClientId"],
Authority
= Configuration["Authentication:AzureAd:AADInstance"]
+ Configuration["Authentication:AzureAd:TenantId"],
ResponseType = OpenIdConnectResponseType.IdToken,
SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme
});
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
TokenValidationParameters = new TokenValidationParameters
{
ClockSkew = TimeSpan.FromMinutes(1),
IssuerSigningKey = TokenAuthenticationOptions.Credentials.Key,
ValidateAudience = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidAudience = TokenAuthenticationOptions.Audience,
ValidIssuer = TokenAuthenticationOptions.Issuer
}
});
Use the OpenIdConnectDefaults.AuthenticationScheme constant when you add the authorization policy and when you add the authentication middleware.
Here you are using OpenIdConnectDefaults. Good. Keep that line.
services.AddAuthorization(configuration =>
{
...
configuration.AddPolicy("OpenIdConnect", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(OpenIdConnectDefaults.AuthenticationScheme) // keep
.RequireAuthenticatedUser().Build());
});
Here you are using CookieAuthenticationDefaults. Delete that line.
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
...
SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme // delete
});
Why?
When your OpenIdConnect authorization policy runs, it will look for an authentication scheme named OpenIdConnectDefaults.AuthenticationScheme. It will not find one, because the registered OpenIdConnect middleware is named CookieAuthenticationDefaults.AuthenticationScheme. If you delete that errant line, then the code will automatically use the appropriate default.
Edit: Commentary on the sample
A second reasonable solution
The linked sample application from the comments calls services.AddAuthentication and sets SignInScheme to "Cookies". That changes the default sign in scheme for all of the authentication middleware. Result: the call to app.UseOpenIdConnectAuthentication is now equivalent to this:
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme
}
That is exactly what Camilo had in the first place. So why did my answer work?
My answer worked because it does not matter what SignInScheme name we choose; what matters is that those names are consistent. If we set our OpenIdConnect authentication sign in scheme to "Cookies", then when adding an authorization policy, we need to ask for that scheme by name like this:
services.AddAuthorization(configuration =>
{
...
configuration.AddPolicy("OpenIdConnect", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme) <----
.RequireAuthenticatedUser().Build());
});
A third reasonable solution
To emphasize the importance of consistency, here is a third reasonable solution that uses an arbitrary sign in scheme name.
services.AddAuthorization(configuration =>
{
configuration.AddPolicy("OpenIdConnect", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes("Foobar")
.RequireAuthenticatedUser().Build());
});
Here you are using CookieAuthenticationDefaults. Delete that line.
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
SignInScheme = "Foobar"
});

Categories