Dotnet core authorization with multiple schemes - asp.net-core

I have a dotnet core v3 web api that uses both Azure AD and Api key authentication. I would like to use bearer tokens on most controller methods but a few require api key access. The bearer token authentication and authorization is working fine. But I am unable to get the api key auth going.
Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(Configuration, "AzureAd")
.EnableTokenAcquisitionToCallDownstreamApi()
.AddInMemoryTokenCaches();
services.AddAuthentication()
.AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>("Api-Key", null);
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder(
JwtBearerDefaults.AuthenticationScheme,
"Api-Key")
.RequireAuthenticatedUser()
.Build();
options.AddPolicy("RequireAdministratorRole",
policy => policy.RequireRole("Administrator"));
options.AddPolicy("RequireApiKeyRole",
policy => policy.RequireRole("ApiKeyRole"));
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers().RequireAuthorization();
});
}
The api key authentication handler
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var authorizationHeaders = Request.Headers["Authorization"];
var apiKeyHeader = authorizationHeaders.FirstOrDefault(
header => header.StartsWith(Scheme.Name, StringComparison.OrdinalIgnoreCase));
string apiKey = apiKeyHeader.Substring(Scheme.Name.Length).Trim();
var claims = new List<Claim>
{
new Claim(ClaimTypes.Role, "ApiKeyRole")
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
Controller
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
[HttpGet]
[Authorize(Policy = "RequireApiKeyRole")]
public ActionResult Get()
{
return Ok("Success");
}
}
This does not work. I am getting authenticated and the role is set. But I get a 403 response.

Related

ASP.NET Core Web Api - Problem with using session

I am developing an asp.net core web api and I want to store a token that will be sent to my endpoints in order for a user to authenticate. For that I have some requirements which force me to implement an own authentication method. I inherit from AuthenticationHandler and implement the HandleAuthenticateAsync method:
public AuthenticateHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
IHttpContextAccessor httpContextAccessor)
: base(options, logger, encoder, clock)
{
AuthenticateHandlerHelperFunctions = new AuthenticateHandlerHelperFunctions();
_checkAccessTokenModel = new CheckAccessTokenModel();
session = httpContextAccessor.HttpContext.Session;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
//before this: check header and get authorization informations
string submittedToken = authorizationheader.Substring("bearer".Length).Trim();
try
{
if (string.IsNullOrEmpty(session.GetString("Token")))
{
_checkAccessTokenModel = await AuthenticateHandlerHelperFunctions.CheckAccessToken(submittedToken);
if(_checkAccessTokenModel.Active == false)
{
_failReason = "Token not valid anymore, request another one!";
return AuthenticateResult.Fail("Token not valid anymore, request another one!");
}
session.SetString("Token", submittedToken);
}
}
catch
{
return AuthenticateResult.Fail("Invalid Authorization Header");
}
var claims = new[] {
new Claim(ClaimTypes.Name, _checkAccessTokenModel.Exp.ToString()),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
The goal is to use the session to save the token and not execute the CheckAccessToken method for every request. I will get frequent data on the endpoints that are configured with [Authorize] so I want to save computing time. I looked this up and most of the errors where problems with the startup where the app.UseSession() was not set correctly etc. but not in my case I believe. Here is my Startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "DigVPapi", Version = "v1" });
});
services.AddDbContextFactory<AntragDBNoInheritanceContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
});
services.AddAuthentication("BasicAuthentication")
.AddScheme<AuthenticationSchemeOptions, AuthenticateHandler>("BasicAuthentication", null);
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = System.TimeSpan.FromSeconds(10);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
services.AddHttpContextAccessor();
services.AddSingleton<IJWTManagerRepository, JWTManagerRepository>();
services.AddControllers();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "DigVPapi v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
If this is not possible. What could I do instead to save the token in some different way? Of course I could save the token in the database but this would only move the problem to a database query tha twould be made every time. The error that I get when trying to authenticate is following
System.InvalidOperationException: 'Session has not been configured for this application or request.'

Multiple authentication schemes in ASP.NET Core 5.0 WebAPI

I have a full set of (ASP.NET Core) web APIs developed in .NET 5.0 and implemented Cookies & OpenIdConnect authentication schemes.
After successful authentication (user id and password) with Azure AD, cookie is generated and stores user permissions etc.
Now, I would like to expose the same set of APIs to a third party consumer using API Key based authentication (via api-key in the request headers).
I have developed a custom authentication handler as below.
using Microsoft.AspNetCore.Authentication;
namespace Management.Deployment.Securities.Authentication
{
public class ApiKeyAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
}
}
namespace Management.Deployment.Securities.Authentication
{
public static class ApiKeyAuthenticationDefaults
{
public static readonly string AuthenticationScheme = "ApiKey";
public static readonly string DisplayName = "ApiKey Authentication Scheme";
}
}
ApiKeyAuthenticationHandler is defined as below, straight forward, if the request headers contain the valid api key then add permissions claim (assigned to the api key) and mark the authentication as success else fail.
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Management.Securities.Authorization;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
namespace Management.Deployment.Securities.Authentication
{
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationSchemeOptions>
{
private const string APIKEY_NAME = "x-api-key";
private const string APIKEY_VALUE = "sdflasuowerposaddfsadf1121234kjdsflkj";
public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
string extractedApiKey = Request.Headers[APIKEY_NAME];
if (!APIKEY_VALUE.Equals(extractedApiKey))
{
return Task.FromResult(AuthenticateResult.Fail("Unauthorized client."));
}
var claims = new[]
{
new Claim("Permissions", "23")
};
var claimsIdentity = new ClaimsIdentity(claims, nameof(ApiKeyAuthenticationHandler));
var authenticationTicket = new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(authenticationTicket));
}
}
}
I have also defined ApiKeyAuthenticationExtensions as below.
using Microsoft.AspNetCore.Authentication;
using Management.Deployment.Securities.Authentication;
using System;
namespace Microsoft.Extensions.DependencyInjection
{
public static class ApiKeyAuthenticationExtensions
{
public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder)
{
return builder.AddScheme<ApiKeyAuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationDefaults.AuthenticationScheme, ApiKeyAuthenticationDefaults.DisplayName, x => { });
}
public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder, Action<ApiKeyAuthenticationSchemeOptions> configureOptions)
{
return builder.AddScheme<ApiKeyAuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationDefaults.AuthenticationScheme, ApiKeyAuthenticationDefaults.DisplayName, configureOptions);
}
}
}
Skimmed version of ConfigureServices() in Startup.cs is here. Please note I have used ForwardDefaultSelector.
public void ConfigureServices(IServiceCollection services)
{
IAuthCookieValidate cookieEvent = new AuthCookieValidate();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.Name = ".Mgnt.AspNetCore.Cookies";
options.ExpireTimeSpan = TimeSpan.FromDays(1);
options.Events = new CookieAuthenticationEvents
{
OnRedirectToAccessDenied = context =>
{
context.Response.StatusCode = 403;
return Task.FromResult(0);
},
OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return Task.FromResult(0);
},
OnValidatePrincipal = cookieEvent.ValidateAsync
};
options.ForwardDefaultSelector = context =>
{
return context.Request.Headers.ContainsKey(ApiConstants.APIKEY_NAME) ? ApiKeyAuthenticationDefaults.AuthenticationScheme : CookieAuthenticationDefaults.AuthenticationScheme;
};
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
})
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => Configuration.Bind(OpenIdConnectDefaults.AuthenticationScheme, options))
.AddApiKey();
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
});
services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationPolicyProvider>();
services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
}
The Configure method is as below.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHsts();
app.Use((context, next) =>
{
context.Request.Scheme = "https";
return next();
});
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
When I send the correct apikey in the request headers, the custom handler is returning success as authentication result and the request is processed further.
But if an incorrect api key is passed, it is not returning the authentication failure message - "Unauthorized client.". Rather, the request is processed further and sending the attached response.
What changes to be made to resolve this issue so the api returns the authentication failure message - "Unauthorized client." and stops further processing of the request?
if you plan to use apikeys, then you are on your own and there is (as far as I know) no built in direct support for API-keys. There is however built in support for JWT based access tokens and I would recommend that you use that as well for external third parties who wants to access your api. Perhaps using client credentials flow.
For some help, see http://codingsonata.com/secure-asp-net-core-web-api-using-api-key-authentication/
I also think you should configure and let the authorization handler be responsible for deciding who can access the services.
see Policy-based authorization in ASP.NET Core

How to Authorize Controller .NET Core API

I'm able to generate tokens succesfully when user login the app.But after I added [Authorize] on my controller,that token comes from header cannot pass the authorization.On Postman returns Unauthorized even though sending up-to date token in header to controller.Before added [Authorize] that worked very well
Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
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.AddDbContext<DataContext>(x => x.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
services.AddControllers().AddNewtonsoftJson(opt => {
opt.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
});
services.AddCors();
services.AddAutoMapper(typeof(AppointmentRepository).Assembly);
services.AddScoped<IHospitalRepository, HospitalRepository>();
services.AddScoped<IAppointmentRepository, AppointmentRepository>();
services.AddScoped<IPatientRepository, PatientRepository>();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII
.GetBytes(Configuration.GetSection("AppSettings:Token").Value)),
ValidateIssuer = false,
ValidateAudience = false
};
});
services.AddControllers();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseCors(x => x.WithOrigins().AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
login Method in controller
[HttpPost("login")]
public async Task<IActionResult> Login(PatientLoginDto patientLoginDto)
{
//if user exists or not
var patientFromRepo = await _repo.Login(patientLoginDto.IdentityNumber, patientLoginDto.Password);
if (patientFromRepo == null)
{ return Unauthorized(); }
var claims = new[]
{
//Token has two claim username and id
new Claim(ClaimTypes.NameIdentifier,patientFromRepo.Id.ToString()),
new Claim(ClaimTypes.NameIdentifier,patientFromRepo.Name)
};
//key generated
var key = new SymmetricSecurityKey(Encoding.UTF8
.GetBytes(_config.GetSection("AppSettings:Token").Value));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);
var tokenDescriptor = new SecurityTokenDescriptor
{
//passing claims
Subject = new ClaimsIdentity(claims),
//expiry date in hours
Expires = DateTime.Now.AddDays(1),
SigningCredentials = creds
};
var tokenHandler = new JwtSecurityTokenHandler();
//storing token here(based on token descriptor object)
var token = tokenHandler.CreateToken(tokenDescriptor);
var patient = _mapper.Map<PatientLoggedinDto>(patientFromRepo);
return Ok(new
{
//as response send back to the client
token = tokenHandler.WriteToken(token),
patient
});
}
}
You need register the AuthenticationMiddleware before app.UseAuthorization();:
app.UseRouting();
app.UseAuthentication(); // add this line. NOTE The order is important.
app.UseAuthorization();
// ... other middlewares

Authorize doesn't work in Signalr of ASP.NET Core 2.1

I've upgraded my project from ASP.Net Core 2.0 to ASP.NET Core 2.1 by following this tutorial.
Everything was fine until I applied Signar Core 2.1 to my project.
This is my Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
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.AddSingleton<IAuthorizationHandler, SolidAccountRequirementHandler>();
services.AddCors(
options => options.AddPolicy("AllowCors",
builder =>
{
builder
.AllowAnyOrigin()
.AllowCredentials()
.AllowAnyHeader()
.AllowAnyMethod();
})
);
services.AddAuthorization(x =>
{
x.AddPolicy("MainPolicy", builder =>
{
builder.Requirements.Add(new SolidAccountRequirement());
});
});
services.AddSignalR();
#region Mvc builder
var authenticationBuilder = services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);
authenticationBuilder.AddJwtBearer(o =>
{
// You also need to update /wwwroot/app/scripts/app.js
o.SecurityTokenValidators.Clear();
// Initialize token validation parameters.
var tokenValidationParameters = new TokenValidationParameters();
tokenValidationParameters.ValidAudience = "audience";
tokenValidationParameters.ValidIssuer = "issuer";
tokenValidationParameters.IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("SigningKey"));
tokenValidationParameters.ValidateLifetime = false;
o.TokenValidationParameters = tokenValidationParameters;
});
// Construct mvc options.
services.AddMvc(mvcOptions =>
{
////only allow authenticated users
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.AddRequirements(new SolidAccountRequirement())
.Build();
mvcOptions.Filters.Add(new AuthorizeFilter(policy));
})
.AddJsonOptions(options =>
{
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1); ;
#endregion
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
//app.UseHttpsRedirection();
app.UseCors("AllowCors");
app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("/chathub");
});
app.UseMvc();
}
}
This is my SolidRequirementHandler
public class SolidAccountRequirementHandler : AuthorizationHandler<SolidAccountRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SolidAccountRequirement requirement)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
}
This is my ChatHub.cs:
public class ChatHub : Hub
{
[Authorize(Policy = "MainPolicy")]
public override Task OnConnectedAsync()
{
return base.OnConnectedAsync();
}
}
What I expected was MainPolicy would be called when I used my AngularJS app to connect to ChatHub. However, OnConnectedAsync() function was called without checking request identity.
The policy of MVC Controller was applied successfully, but Signalr's doesn't.
Can anyone help me please ?
Thank you,
I posted this question onto Signalr github issue page.
Here is the answer they gave me .
I tried and it worked successfully:
The solution is to put [Authorize] attribute onto ChatHub
[Authorize(Policy = "MainPolicy")]
public class ChatHub : Hub
{
public override Task OnConnectedAsync()
{
return base.OnConnectedAsync();
}
}
Just share to who doesn't know :)
I have the same problem, there are four key things:
1- In your Startup.cs be aware of this Order inside Configure(IApplicationBuilder app)
app.UseRouting();
app.UseAuthorization( );
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<myChat>("/chat");
});
the app.UseAuthorization( ); should always be between app.UseRouting(); and app.UseEndpoints().
2- SignalR doesn't send Tokens in Header but it sends them in Query. In your startup.cs inside ConfigureServices(IServiceCollection services) You have to configure your app in a way to read Tokens from the query and put them in the header. You can Configure your JWT in this way:
services.AddAuthentication()
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidIssuer = [Issuer Site],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes([YOUR SECRET KEY STRING]))
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var path = context.Request.Path;
var accessToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/chat"))
{
context.Request.Headers.Add("Authorization", new[] { $"Bearer {accessToken}" });
}
return Task.CompletedTask;
}
};
});
3- Your Client should send Token when it wants to establish a connection. You can add token to Query when building the connection.
var connection = new signalR.HubConnectionBuilder().withUrl(
"http://localhost:5000/chat", {
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
accessTokenFactory: () => "My Token Is Here"}).build();
4- I didn't add a default Athuentication scheme inside services.AddAuthentication()
So every time I have to specify my authorization scheme like this. [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
And Finally, You can Protect your Chat Class Like this
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication.JwtBearer;
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class myChat : Hub
{
///Some functions
}
It seems that Using statements is important, So make sure using the right ones.
SignalR hub Authorize attribute doesn't work
Note: I have a problem with Authorizing only a single method in the myChat class. I don't know why.

How do I integration test a ASP 5/Core Web API with [Authorize] Attributes

I currently have an ASP 5/ASP Core Web API that I need to integration test with the OWIN Test Server.
The problem is that I use IdentityServer as the authorization server in production and I do not want to include the authorization as part of my integration testing.
This is the Startup.cs of the API:
public Startup(IHostingEnvironment env)
{
// Set up configuration sources.
IConfigurationBuilder builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", true);
if (env.IsEnvironment("Development"))
{
// This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately.
builder.AddApplicationInsightsSettings(developerMode: true);
}
builder.AddEnvironmentVariables();
Configuration = builder.Build().ReloadOnChanged("appsettings.json");
}
public IConfigurationRoot Configuration { get; set; }
// This method gets called by the runtime. Use this method to add services to the container
public IServiceProvider ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddApplicationInsightsTelemetry(Configuration);
ConfigureEntityFrameworkDatabase(services, Configuration);
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<HoehenSuchtIdentityDbContext>()
.AddDefaultTokenProviders();
ConfigureMvc(services);
// register autofac as dependency resolver
ContainerBuilder containerBuilder = new ContainerBuilder();
// register all required autofac modules
RegisterAutofacModules(containerBuilder);
// register all automapper mappings as di services so there dependencies can be resolved
ConfigureAutomapper(containerBuilder);
ConfigureSwagger(services);
// copy all asp core dependency injection registrations to autofac
containerBuilder.Populate(services);
IContainer container = containerBuilder.Build();
return container.Resolve<IServiceProvider>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseBrowserLink();
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
// make sure the database was created and all migrations applied
MigrateDatabase(app);
app.ApplicationServices.GetService<HoehenSuchtDbContext>().EnsureSeedData(env);
app.UseIISPlatformHandler();
app.UseApplicationInsightsRequestTelemetry();
app.UseApplicationInsightsExceptionTelemetry();
ConfigureIdentityServer(app, Configuration);
app.UseStaticFiles();
app.UseMvc();
//app.UseSwaggerGen(/*routeTemplate: "docs/{apiVersion}/swagger.json"*/);
//app.UseSwaggerUi(/*baseRoute: "docs", swaggerUrl: "docs/v1/swagger.json"*/);
}
public static Action<IServiceCollection, IConfigurationRoot> ConfigureEntityFrameworkDatabase = (services, config) =>
{
services.AddEntityFramework()
.AddSqlServer()
.AddDbContext<HoehenSuchtDbContext>(builder =>
builder.UseSqlServer(config["Data:DefaultConnection:ConnectionString"]))
.AddDbContext<HoehenSuchtIdentityDbContext>(builder =>
builder.UseSqlServer(config["Data:IdentityConnection:ConnectionString"]));
};
public static Action<IServiceCollection> ConfigureMvc = services =>
{
services.AddMvc().AddControllersAsServices(new List<Assembly> { typeof(Startup).GetTypeInfo().Assembly });
};
I already tried registering a special test middleware that in theory should authenticate and set a claims principal. But somewhere down the OWIN pipeline the authentication is denied and I get a 401 error code.
This is how I setup the OWIN Test Server:
Startup.MigrateDatabase = app =>
{
app.ApplicationServices.GetService<HoehenSuchtDbContext>().Database.EnsureCreated();
};
Startup.ConfigureEntityFrameworkDatabase = ApiTestServer.ConfigureInMemoryDatabase;
Startup.ConfigureIdentityServer = (app, config) =>
{
app.ApplicationServices.GetService<HoehenSuchtDbContext>().EnsureSeedData(new HostingEnvironment {EnvironmentName = "development" });
app.UseMiddleware<AuthenticatedTestRequestMiddleware>();
};
Server = new TestServer(TestServer.CreateBuilder().UseStartup<Startup>());
And this is my custom AuthenticatedTestRequestMiddleware:
public class AuthenticatedTestRequestMiddleware
{
public const string TestingCookieAuthentication = "TestCookieAuthentication";
public const string TestingHeader = "X-Integration-Testing";
public const string TestingHeaderValue = "78EAAA45-E68B-43C7-9D12-3A5F1E646BD5";
private readonly RequestDelegate _next;
public AuthenticatedTestRequestMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
if (context.Request.Headers.Keys.Contains(TestingHeader) && context.Request.Headers[TestingHeader].First().Equals(TestingHeaderValue))
{
// fake authenticated the user
ClaimsIdentity claimsIdentity = new ClaimsIdentity();
claimsIdentity.AddClaims(new List<Claim>
{
new Claim(ClaimTypes.Name, "admin"),
new Claim(ClaimTypes.NameIdentifier, UserSeedData.AdminUserId)
});
ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
context.User = claimsPrincipal;
}
await _next(context);
}
}
The principal is set and exists in the database with the given ID, but after I call next(context) I get an 401 Unauthorized result.
How can I successfully fake authenticate the user and bypass the [Authorize] while also setting the current User for the HttpRequest?
UPDATE:
If I register my own CookieAuthentication handler like that:
app.UseCookieAuthentication(options =>
{
options.AuthenticationScheme = AuthenticatedTestRequestMiddleware.TestingCookieAuthentication;
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
});
I get the 302 Redirect to the login page. The signin however is working correctly when I use this inside the TestMiddleware await context.Authentication.SignInAsync(TestingCookieAuthentication, claimsPrincipal)
Ok so I found out why it does not work :)
When creating the ClaimsPrincipal the AuthenticationProvider must be included in the constructor of the principal. If the authentication type is not provided the SignInAsync() function will fail and not authenticated the user.
Instead of doing this:
ClaimsIdentity claimsIdentity = new ClaimsIdentity(new List<Claim>
{
new Claim(ClaimTypes.Name, "admin"),
new Claim(ClaimTypes.NameIdentifier, UserSeedData.AdminUserId)
});
You must specify the AuthenticationHandler like this:
ClaimsIdentity claimsIdentity = new ClaimsIdentity(new List<Claim>
{
new Claim(ClaimTypes.Name, "admin"),
new Claim(ClaimTypes.NameIdentifier, UserSeedData.AdminUserId)
}, TestingCookieAuthentication);