.NET Core API Problem using multiple authentication schemes - c#

I am having some difficulty getting multiple authenticaion schemes to work with my .NET Core API.
Each works perfectly when set as the Default Scheme to be used, but I cannot get them to chain authenticaion, so if one fails authentication it moves onto the next one in the chain to try and authenticate using that scheme.
I'm using
JWT Bearer Token Authentication
API Key Authentication
As can be seen in the code below I've setup an AuthorizeFilter policy with the multiple schemes, but obviously I'm missing something somewhere along the way and I'm hoping you may be able to spot what I am missing.
Below is the complete start up code I am using.
Kind Regards
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(authOptions =>
{
authOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
authOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(bearerOptions =>
{
bearerOptions.RequireHttpsMetadata = true;
bearerOptions.SaveToken = true;
bearerOptions.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = JWTProvider.CLAIM_ISSUER,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Convert.FromBase64String(this.Configuration[JWTSecurityKeyProvider.DEFAULT_JWT_SECURITY_KEYNAME])) { KeyId = "416" },
ValidAudience = JWTProvider.API_AUDIENCE,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(1)
};
})
.AddApiKeySupport(options =>
{
});
services.AddControllers(options =>
{
var defaultSchemes = new[] { JwtBearerDefaults.AuthenticationScheme, ApiKeyAuthenticationOptions.DefaultScheme };
var defaultPolicy = new AuthorizationPolicyBuilder(defaultSchemes)
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(defaultPolicy));
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Auth API", Version = "v1" });
});
services.AddSingleton<JWTSecurityKeyProvider>();
services.AddSingleton<JWTProvider>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Auth API v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}

According to documentation:
Only one JWT bearer authentication is registered with the default authentication scheme JwtBearerDefaults.AuthenticationScheme. Additional authentication has to be registered with a unique authentication scheme.
Also it rewrites default policy in services.AddAuthorization() insted of services.AddControllers()

In case anyone else experiences this issue, to sort out the problem you need to include the Authorized Schemes as an attribute on each controller. Then everything is works tickety-boo.
E.g.
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme + "," + ApiKeyAuthenticationOptions.DefaultScheme)]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : BaseAuthenticatedController
{

Related

IdentityServer4 sometimes returns 500 for google and microsoft authentication

Sometimes google and microsoft authentication returns 500 on IdentityServer4.
Specifically, /signin-google and /signin-microsoft are returning 500 errors, and the situation is as follows
・About 3,000 requests per minute.
・About 80 of them return 500 errors.
・The rest return 301.
Do you know the cause?
Is there a problem with the number of accesses or the external authentication side?
Also, will upgrading to IdenityServer 6 and registering a license solve the problem?
Here is the code
Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
AppSettings.Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityServer(options =>
{
options.Caching.ClientStoreExpiration = TimeSpan.FromMinutes(60);
options.Caching.ResourceStoreExpiration = TimeSpan.FromMinutes(60);
options.Caching.CorsExpiration = TimeSpan.FromMinutes(60);
})
.AddRedirectUriValidator<RedirectUriValidator>()
.AddInMemoryCaching()
.AddInMemoryIdentityResources(new IdentityResource[]
{
new IdentityResources.OpenId(), // OIDC認証を使用
new IdentityResources.Profile(),
})
.AddInMemoryApiScopes(new ApiScope[]
{
new ApiScope(IdentityServerConstants.LocalApi.ScopeName),
})
.AddInMemoryPersistedGrants()
.AddInMemoryCaching()
.AddInMemoryClients(Configuration.GetSection("Clients"))
.AddDeveloperSigningCredential()
.AddAspNetIdentity<User>();
services.ConfigureApplicationCookie(config =>
{
config.LoginPath = "/Web/User/Login";
config.LogoutPath = "/Web/User/Logout";
config.ExpireTimeSpan = TimeSpan.FromSeconds(Common.LoginTimeoutSeconds);
});
services.AddAuthentication(options =>
{
options.DefaultScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Common.TokenCreateKey)),
ClockSkew = TimeSpan.Zero,
};
}).AddGoogle(options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = Configuration.GetValue<string>("GoogleClient:ClientId");
options.ClientSecret = Configuration.GetValue<string>("GoogleClient:ClientSecret");
}).AddMicrosoftAccount(options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = Configuration.GetValue<string>("MicrosoftClient:ClientId");
options.ClientSecret = Configuration.GetValue<string>("MicrosoftClient:ClientSecret");
});
services.AddLocalApiAuthentication();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseAuthentication();
app.UseIdentityServer();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
One problem that I see is that you have these in the wrong order:
app.UseAuthorization();
app.UseAuthentication();
app.UseIdentityServer();
Authorization should always be last:
app.UseIdentityServer();
app.UseAuthorization();
If I am not wrong, you can also remove UseAuthentication() because UseIdentityServer adds that for you.

AspNetCore JWT Authentication Without "iat" in token header

I'm trying to configure an authentication in some application that will consume the login validation from another system. Basically, I get a token with the users information, and I have to map it to my database, because the user it was already authenticated. We're doing it with using jwt tokens.
So here's the problem:
The token I'm getting doesn't have "iat" field, and AspNetCore seems to reject the token without that field.
Is there a way to configure the authentication to ignore that field?
Here is the token structure:
TOKEN - HEADER
{
"alg": "HS512"
}
TOKEN - BODY
{
"sub": [mysub],
"user": { ... },
"exp": [timestamp]
}
And here's the configuration:
...
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuerSigningKey = false,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
...
Edit: An earlier version of this answer mixed up iat and nbf. The answer has been rewritten accordingly.
I tried reproducing your issue in an empty project, and was successfully able to validate a token with the structure you outlined in your question (so without the iat claim).
Setup I used to get this to work:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o =>
{
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = false,
ValidateIssuer = false,
ValidateAudience = false,
SignatureValidator = (t, p) => new JwtSecurityToken(t)
};
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
[Authorize]
[Route("debug")]
public class DebugAuthController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok(User.Claims.Single(c => c.Type == "user").Value);
}
I used the following token for testing: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJteXN1YmplY3QiLCJ1c2VyIjp7Im5hbWUiOiJTdGFja092ZXJmbG93In0sImV4cCI6MTU5MDk5NzMyNn0.FVFl6gDYOrmzj7_6OqHPTxU3mfQWs864u7fBLM5ThuM
It's worth double-checking to see if you're calling both UseAuthentication and UseAuthorization in ConfigureServices, and that you're calling them in that order.
Note: Sample code provided disables virtually all validation checks on the JWT for testing purposes. Do not use this code as-is unless you know what you're doing!

JWT authentication with Blazor 0.9.0 and ASP.NET Core 3 preview 4

I followed this tutorial: https://medium.com/#st.mas29/microsoft-blazor-web-api-with-jwt-authentication-part-1-f33a44abab9d (which is for .NET core 2.2).
Here my Startup class
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public IConfiguration Configuration { get; }
public Startup (IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddNewtonsoftJson();
//services.AddMvcCore().AddAuthorization().AddNewtonsoftJson();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseResponseCompression();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBlazorDebugging();
}
app.UseAuthentication();
//app.UseAuthorization();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
app.UseBlazor<Client.Startup>();
}
}
I also added [Authorize] on the Api controller SampleDataController.
I expected (as per the post) to get a 401 (Unauthorized) error when accessing the data, instead I get a complaint about missing authorization middleware
If I add app.UseAuthorization() (un-comment the line) the application works normally, without any error, retrieving the data as though the client is authorized.
What needs to be done to get a 401 when accessing the data?
Place both app.UseAuthentication() and app.UseAuthorization() after app.UseRouting()
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(routes =>
{
routes.MapDefaultControllerRoute();
});
If you send a request with a an authorization token, and the server authorization is not setup in the Startup.cs file, the API will return an error saying <Called method> contains authorization metadata, but a middleware was not found that supports authorization...
The fix is to add the below lines in the Startup.cs file, BETWEEN app.UseRouting() and app.UseEndpoints(...):
app.UseRouting();
//AUTHORIZING
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
});

ASP.NET Core 2.1 API JWT token Session.id changes on every request

I have a asp.net core 2.1 API that I connect to using and Angular 4 app and authenticate thru a JWT token. I also have 2 SignalR hubs there as well.
The authentication works nice and I am sure it works because after login I have access to the methods and classes I have set [Authorize] on.
The problem is the injected _accessor.HttpContext.Session.Id changes every time with every request. So the real issues is I cannot use session variables.
I am at a loss and it seems to me I am missing something here.
Can someone please help me with some ideas? Any help is much appreciated.
This is my startup.cs so far:
public class Startup
{
public Startup(IConfiguration configuration, IHostingEnvironment env)
{
Configuration = configuration;
if (env.IsDevelopment())
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.RollingFile(Path.Combine(env.ContentRootPath+"/logs/", "log-{Date}.txt"))
//, outputTemplate: "{MachineName} {EnvironmentUserName}: {Message:lj}{NewLine}{Exception}"
.WriteTo.Seq("http://192.168.1.164:5341")
.Enrich.WithMachineName()
.Enrich.WithEnvironmentUserName()
.CreateLogger();
}
else
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.RollingFile(Path.Combine(env.ContentRootPath + "/logs/", "log-{Date}.txt"))
.Enrich.WithMachineName()
.Enrich.WithEnvironmentUserName()
.CreateLogger();
}
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
var key = Encoding.ASCII.GetBytes(Configuration.GetSection("AppSettings:Token").Value);
services.AddDbContext<PaymentServicesContext>(options => options.UseSqlServer(Configuration.GetConnectionString("PaymentDatabase")));
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(10);
options.Cookie.HttpOnly = true;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1).AddSessionStateTempDataProvider();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters =
new TokenValidationParameters
{
LifetimeValidator = (before, expires, token, param) =>
{
return expires > DateTime.UtcNow;
},
ValidateAudience = false,
ValidateIssuerSigningKey = true,
ValidateIssuer = false,
ValidateActor = false,
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(key)
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
services.AddAutoMapper();
services.AddCors();
services.AddSignalR(options => options.EnableDetailedErrors = true);
///services
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<IBrainzService, BrainzService>();
services.AddTransient<ISecurityService, SecurityService>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
loggerFactory.AddSerilog();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler(builder =>
{
builder.Run(async context =>
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var error = context.Features.Get<IExceptionHandlerFeature>();
if (error != null)
{
await context.Response.WriteAsync(error.Error.Message);
}
});
});
}
app.UseCors(builder => builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod().AllowCredentials())
.UseStaticFiles()
.UseWebSockets();
app.UseAuthentication();
if (!env.IsDevelopment())
{
app.UseHttpsRedirection();
}
app.UseWebSockets();
app.UseSignalR(
routes =>
{
routes.MapHub<MessagingHub>("/messagingHub");
routes.MapHub<UpdatesHub>("/updatesHub");
});
app.UseSession();
app.UseMvc();
}
}
The configuration works.
The problem was that Angular's HttpClient does not store/get/send cookies by default as I have found out the hard way.
All I had to do was add {withCredentials: true } option to the login request and the session cookie popped into the browser!
To get the session back you have to add this option to all requests and the API will know the session!
One solution would be to create an Interceptor to intercept all requests before it leaves the client application. This tutorial explains it well. You can also do this on a per-request level by setting the withCredentials to true.
const requestOptions = {
headers: new HttpHeaders({
'Authorization': "my-request-token"
}),
withCredentials: true
};

ASP .NET Core 2.0 - JWT External Auth

I am trying to get started with authentication on an ASP.NET Core 2.0 web app.
My company is using Ping Federate and I am trying to authenticate my users using the company login page and in return validating the returned token using my signing key (X509SecurityKey down here).
The login page link looks like:
https://companyname.com/authorization.oauth2?response_type=code&redirect_uri=https%3a%2f%2fJWTAuthExample%2fAccount%2fLogin&client_id=CompanyName.Web.JWTAuthExample&scope=&state=<...state...>
Out of the box, I configured the Startup.cs to be able to log in and challenge against this site.
I decorated my HomeController with a [Authorize(Policy="Mvc")] but when I access one of the pages, I just get a blank page.
Debug is not hitting the OnChallenge or OnAuthenticationFailed methods when I add it to options.Events (I think because user needs to be authenticated first).
So, what am I missing in order for a redirect to my authentication website to happen? Is it built in or do I have to do some manual configuration?
(Note: In other web apps, using asp net framework, I use a redirect in an Authorize attribute when authentication fails)
Related post: Authorize attribute does not redirect to Login page when using .NET Core 2's AddJwtBearer
- From this post, does it mean I am not using the right authentication method? I am building a web app, not an API.
namespace JWTAuthExample
{
public class Startup
{
public Startup(IConfiguration configuration, IHostingEnvironment hostingEnvironment)
{
Configuration = configuration;
HostingEnvironment = hostingEnvironment;
string certificatepath = Path.Combine(HostingEnvironment.ContentRootPath, $"App_Data\\key.cer");
KEY = new X509SecurityKey(new X509Certificate2(certificatepath));
}
public IConfiguration Configuration { get; }
public IHostingEnvironment HostingEnvironment { get; }
private string AUTH_LOGINPATH { get; } = Configuration["DefaultAuth:AuthorizationEndpoint"];
private X509SecurityKey KEY { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.IncludeErrorDetails = true;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
// Ensure token expiry
RequireExpirationTime = true,
ValidateLifetime = true,
// Ensure token audience matches site audience value
ValidateAudience = false,
ValidAudience = AUTH_LOGINPATH,
// Ensure token was issued by a trusted authorization server
ValidateIssuer = true,
ValidIssuer = AUTH_LOGINPATH,
// Specify key used by token
RequireSignedTokens = true,
IssuerSigningKey = KEY
};
});
services.AddAuthorization(options =>
{
options.AddPolicy("Mvc", policy =>
{
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
});
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseBrowserLink();
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
Following Brad's suggestion,
Here is a sample of code to perform an OpenId Connect confirguration on ASP NET 2.0
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = Configuration["AuthoritySite"];
options.ClientId = Configuration["ClientId"];
options.ClientSecret = Configuration["ClientSecret"];
options.Scope.Clear();
// options.Scope.Add("Any:Scope");
options.ResponseType = OpenIdConnectResponseType.CodeIdTokenToken;
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.TokenValidationParameters = new TokenValidationParameters
{
// Compensate server drift
ClockSkew = TimeSpan.FromHours(12),
// Ensure key
IssuerSigningKey = CERTIFICATE,
// Ensure expiry
RequireExpirationTime = true,
ValidateLifetime = true,
// Save token
SaveSigninToken = true
};
});
services.AddAuthorization(options =>
{
options.AddPolicy("Mvc", policy =>
{
policy.AuthenticationSchemes.Add(OpenIdConnectDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
});
});
}
More details here: https://learn.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x?view=aspnetcore-2.1

Categories