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.'
Related
I have a blazor wasm hosted solution that is setup using Role authentication. However, whenever I add an [Authorize] attribute to any of my API Controllers I get a 401 Unauthorized. I know the user has the proper role as the UI is showing and hiding features for that role. Its like the roles are not being passed up to the API. What am I missing?
Server - Starup.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.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
//Register the Datacontext and Connection String
services.AddDbContext<DataContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
//Sets up the default Asp.net core Identity Screens - Use Identity Scaffolding to override defaults
services.AddDefaultIdentity<ApplicationUser>( options =>
{
options.SignIn.RequireConfirmedAccount = true;
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireUppercase = true;
options.Password.RequiredUniqueChars = 0;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequiredLength = 8;
options.User.RequireUniqueEmail = true;
})
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<DataContext>();
//Associates the User to Context with Identity
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, DataContext>( options =>
{
options.IdentityResources["openid"].UserClaims.Add(JwtClaimTypes.Role);
options.ApiResources.Single().UserClaims.Add(JwtClaimTypes.Role);
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove(JwtClaimTypes.Role);
//Adds authentication handler
services.AddAuthentication().AddIdentityServerJwt();
//Register Repositories for Dependency Injection
services.AddScoped<ICountryRepository, CountryRepository>();
services.AddControllersWithViews();
services.AddRazorPages();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DataContext dataContext)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
//AutoMigrates data
dataContext.Database.Migrate();
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseSerilogIngestion();
app.UseSerilogRequestLogging();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
});
}
}
Client - Program.cs
public class Program
{
public static async Task Main(string[] args)
{
//Serilog
var levelSwitch = new LoggingLevelSwitch();
Log.Logger = new LoggerConfiguration()
.MinimumLevel.ControlledBy(levelSwitch)
.Enrich.WithProperty("InstanceId", Guid.NewGuid().ToString("n"))
.WriteTo.BrowserHttp(controlLevelSwitch: levelSwitch)
.CreateLogger();
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddHttpClient("XXX.ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
// Supply HttpClient instances that include access tokens when making requests to the server project
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("XXX.ServerAPI"));
builder.Services.AddApiAuthorization()
.AddAccountClaimsPrincipalFactory<RolesClaimsPrincipalFactory>();
var baseAddress = new Uri($"{builder.HostEnvironment.BaseAddress}api/");
void RegisterTypedClient<TClient, TImplementation>(Uri apiBaseUrl)
where TClient : class where TImplementation : class, TClient
{
builder.Services.AddHttpClient<TClient, TImplementation>(client =>
{
client.BaseAddress = apiBaseUrl;
});
}
RegisterTypedClient<ICountryService, CountryService>(baseAddress);
await builder.Build().RunAsync();
}
}
RolesClaimPrincipalFactory.cs
public class RolesClaimsPrincipalFactory : AccountClaimsPrincipalFactory<RemoteUserAccount>
{
public RolesClaimsPrincipalFactory(IAccessTokenProviderAccessor accessor) : base(accessor)
{
}
public async override ValueTask<ClaimsPrincipal> CreateUserAsync(
RemoteUserAccount account,
RemoteAuthenticationUserOptions options)
{
ClaimsPrincipal user = await base.CreateUserAsync(account, options);
if (user.Identity.IsAuthenticated)
{
var identity = (ClaimsIdentity)user.Identity;
Claim[] roleClaims = identity.FindAll(identity.RoleClaimType).ToArray();
if (roleClaims != null && roleClaims.Any())
{
foreach (Claim existingClaim in roleClaims)
{
identity.RemoveClaim(existingClaim);
}
var rolesElem = account.AdditionalProperties[identity.RoleClaimType];
if (rolesElem is JsonElement roles)
{
if (roles.ValueKind == JsonValueKind.Array)
{
foreach (JsonElement role in roles.EnumerateArray())
{
identity.AddClaim(new Claim(options.RoleClaim, role.GetString()));
}
}
else
{
identity.AddClaim(new Claim(options.RoleClaim, roles.GetString()));
}
}
}
}
return user;
}
}
You are likely having this issue since you are using ICountryService that has it's own http client which is not configured to include auth tokens in the outgoing requests -- no tokens, no access.
We can attach tokens by adding an AuthorizationMessageHandler to the client, just like your named client (XXX.ServerAPI) is configured.
Try changing your typed client helper method to this:
/* Client Program.cs */
void RegisterTypedClient<TClient, TImplementation>(Uri apiBaseUrl)
where TClient : class where TImplementation : class, TClient
{
builder.Services.AddHttpClient<TClient, TImplementation>(
client => client.BaseAddress = apiBaseUrl)
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
}
You probably want to change the helper to also only include tokens to client's that actually require them (if you are using that helper for other clients as well)
See the docs for more info.
I made a CORS policy in my asp.net core API, but it seems the API is accessible for the public, as I can see the JSON by just typing the API URL in browser. So I just want my front-end(which is a angular app in the same host of API) have access to API not everyone.
this is my start up class:
public class Startup
{
readonly string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("myurl")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
services.AddControllers().AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
options.JsonSerializerOptions.DictionaryKeyPolicy = null;
});
services.Configure<FormOptions>(o =>
{
o.ValueLengthLimit = int.MaxValue;
o.MultipartBodyLengthLimit = int.MaxValue;
o.MemoryBufferThreshold = int.MaxValue;
});
services.AddControllers();
string connectionString = "xxx";
services.AddDbContext<decorContext>(
option => option.UseSqlServer(connectionString)
);
}
// 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.UseDefaultFiles();
app.UseStaticFiles();
app.UseStaticFiles(new StaticFileOptions()
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory() ,#"StaticFiles")),
RequestPath = new PathString("/StaticFiles")
});
app.UseRouting();
app.UseCors(MyAllowSpecificOrigins);
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
CORS is kicked in when Domain A accesses Domain B (like making an ajax call in js) and it won't apply when you hit the url directly. That's why it's called CROSS-ORIGIN.
You don't typically implement "script -> website" restriction. You could restrict access by location/IP, user identity/role/claims, or by checking if antiforgery token is present in a request. Check out Overview of ASP.NET Core Security
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.
I am trying to configure cookie authentication with identity and ef. So far I am able to have a valid Set-Cookie in my Controller response. The browser sends this cookie back, but the AuthorizeFilter always redirects to Login page, thus Authentication doesn't seem to work. What am I suppoed to configure?
Here's my ConfigureServices in Startup so far:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddCors(o => o.AddPolicy("Cors", builder =>
{
builder.WithOrigins(Configuration["AllowedOrigins"].Split(","))
.AllowAnyMethod()
.AllowCredentials()
.AllowAnyHeader();
}));
services.AddDbContext<MyIdentityDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<MyIdentityDbContext>()
.AddDefaultTokenProviders();
services.ConfigureApplicationCookie(options => {
if (!String.IsNullOrEmpty(Configuration["AuthCookieDomain"]))
{
options.Cookie.Domain = Configuration["AuthCookieDomain"];
}
options.Cookie.Name = Configuration["AuthCookieName"];
options.Cookie.HttpOnly = false;
options.Cookie.SameSite = SameSiteMode.None;
});
}
Then, my Configure in Startup:
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
IServiceProvider serviceProvider)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors("Cors");
app.UseMvc();
app.UseAuthentication();
}
Then, my Action that actually successfully sets the cookie
// GET api/values
[HttpPost]
public async Task<ActionResult> Post([FromBody] AuthPost post)
{
if (post == null || String.IsNullOrEmpty(post.UserName) || String.IsNullOrEmpty(post.Password))
{
return BadRequest();
}
var result = await signInManager.PasswordSignInAsync(post.UserName, post.Password, true, false);
if (result.Succeeded)
{
return Ok();
}
return Unauthorized();
}
And finally, my other Action with Authorize attribute that does not work (Always redirects to Login)
[HttpGet]
[Authorize]
public async Task<ActionResult> Get()
{
var user = await userManager.GetUserAsync(User);
return Ok(new { UserName = user.UserName });
}
OK, ConfigureApplicationCookie is the way to work. What caused the issue was the WRONG order of app.UseMvc(); and app.UseAuthentication();
app.UseAuthentication() must be invoked before app.UseMvc()!
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);