Passing claims into HandleAuthenticateAsync for integration testing - c#

While following Microsoft's ASP.NET Core guide for integration testing authentication, I have the following test built for Authentication:
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication("Test")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"Test", options => {});
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Test");
//Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
What I want to do, is use the [Theory] option instead of [Fact] to test multiple Authentications so it will look like this:
[Theory]
[InlineData("TestAuth1","12345")]
[InlineData("TestAuth2","23456")]
[InlineData("TestAuth3","34567")]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser(string claim, string claimsIdentity)
{
var claim = new Claim(claim, claimsIdentity);
.
.
.
However I'm not sure how to pass claim to TestAuthHandler through AddScheme<AuthenticationSchemeOptions, TestAuthHandler>
Here is the given TestAuthHandler
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "Test");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
I would like to replace the claims variable in HandleAuthenticaAsync() with the claim passed into Get_SecurePageIsReturnedForAnAuthenticatedUser(Claim claim)
As a note they do have to be tested individually since my current authentication will pass as long as one correct authentication exists in the HandleAuthenticateAsync claims variable.
Thank you for any help provided.

I had a similar problem and was able to solve it by creating a custom TestAuthenticationSchemeOptions which implements AuthenticationSchemeOptions which would have a Claims property similar to the example in this link below
How to claim role for HttpContext.User.IsInRole method check in integration test?
e.g.
you would have
public class TestAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
public IEnumerable<Claim> Claims { get; set; }
}
In your test
// Arrange
var inputClaims = new List<Claim> { new Claim(ClaimTypes.Name, "Test user") };
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication("Test")
.AddScheme<TestAuthenticationSchemeOptions, TestAuthHandler>(
"Test", options => { options.Claims = inputClaims; });
});
})
then in your AuthHandler you can grab the claims using Options.Claims
public class TestAuthHandler : AuthenticationHandler<TestAuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<TestAuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = Options.Claims;
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "Test");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}

Related

AllowAnonymous not working with basic authentication

I have enabled basic authentication in a web api:
// configure basic authentication
builder.Services.AddAuthentication("BasicAuthentication")
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);
builder.Services.AddAuthorization();
BasicAuthenticationHandler:
public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly IUserRepositoryService _userRepo;
public BasicAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
IUserRepositoryService userRepo
) : base(options, logger, encoder, clock)
{
_userRepo = userRepo;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var authHeader = Request.Headers["Authorization"].ToString();
if (authHeader != null && authHeader.StartsWith("basic", StringComparison.OrdinalIgnoreCase))
{
var token = authHeader.Substring("Basic ".Length).Trim();
System.Console.WriteLine(token);
var credentialstring = Encoding.UTF8.GetString(Convert.FromBase64String(token));
var credentials = credentialstring.Split(':');
if (await _userRepo.ValidateCredentialsAsync(credentials[0], credentials[1]))
{
// var uid = _userRepo.GetUserIdAsync(credentials[0], credentials[1]);
var claims = new[] {
new Claim(ClaimTypes.Name, credentials[0]),
//new Claim(ClaimTypes.NameIdentifier, uid)
};
var identity = new ClaimsIdentity(claims, "Basic");
var claimsPrincipal = new ClaimsPrincipal(identity);
return AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, Scheme.Name));
}
}
Response.StatusCode = 401;
Response.Headers.Add("WWW-Authenticate", "Basic realm=\"API\"");
return AuthenticateResult.Fail("Invalid Authorization Header");
}
}
This works globally as expected but when trying to use [AllowAnonymous] it doesn't work. The login prompt always pops-up from the BasicAuthenticationHandler. I'm not sure how to detect the attribute when inside the AuthenticateResult.
Should I create a custom [BasicAuth] attribute to enforce authentication instead?

Blazor server authentication JWT for SignalR everything but web applications

I hope someone could clear up a couple of concepts for me.
I use WinForms and Blazor Server and I desire to send a message to a user.
For that purpose, I've decided to try JWT authentication.
I would also prefer the Blazor server have this JWT authentication built in as per documentation
Blazor server:
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
// Add services to the container.
services.AddCors(option => option.AddPolicy("CorsPolicy", p => p.AllowAnyHeader().AllowAnyOrigin().AllowAnyMethod().AllowCredentials()));
//services.AddScoped<AuthenticationStateProvider, MyAuthenticationStateProvider>();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters() //Guessing this section is for security of the token - ensures that I'm the one that made it and such.
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateLifetime = false,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes("test")),
ValidIssuer = "test",
ValidAudience = "test"
};
options.Events = new()
{
OnMessageReceived = (context) =>
{
var path = context.HttpContext.Request.Path;
if (path.StartsWithSegments("/hubs")) // || path.StartsWithSegments("/token")
{
var accessToken = string.IsNullOrWhiteSpace(context.Request.Query["access_token"]) ? context.Request.Headers["Authorization"] : context.Request.Query["access_token"];
if (!string.IsNullOrWhiteSpace(accessToken))
{
//Real
context.Token = accessToken; //another guess - this is adding the accesstoken to the httpContext so it can be used somewhere else probably.
//Test attach claims to context. I want to be able to do this somewhere else though.
var claims = new Claim[]
{
new(ClaimTypes.Name, "myUserName"),
};
var identity = new ClaimsIdentity(claims);
context.Principal = new(identity);
context.Success();
}
}
return Task.CompletedTask;
},
OnAuthenticationFailed = (context) =>
{
Debug.WriteLine("OnAuthenticationFailed: " + context.Exception.Message);
return Task.CompletedTask;
}
};
});
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSignalR()
.AddHubOptions<ChatHub>(options => options.EnableDetailedErrors = true);
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
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();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapBlazorHub();
app.MapHub<ChatHub>("/hubs/chathub");
app.MapFallbackToPage("/_Host");
Hub:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] //Same as: [Authorize(AuthenticationSchemes = "Bearer")]
public class ChatHub : Hub
{
public Task SendMessageAsync(string user, string message)
{
Debug.WriteLine(Context.UserIdentifier); //null
bool test1 = Context.User.Identity.IsAuthenticated; //false
string test2 = Context?.User?.Identity?.Name; //myUserName
return Clients.User(Context?.User?.Identity?.Name).SendAsync("ReceiveMessage", user, message); //This does then not work ofc.
}
}
Client:
HubConnection connection;
string url = "https://localhost:7041/hubs/chathub";
string token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
public Form1()
{
InitializeComponent();
connection = new HubConnectionBuilder()
.WithUrl(url, options =>
{
options.AccessTokenProvider = () => Task.FromResult(token);
})
.WithAutomaticReconnect()
.Build();
}
private async void HubConnectBtn_Click(object sender, EventArgs e)
{
connection.On<string, string>("ReceiveMessage", (user, message) =>
{
this.Invoke(() =>
{
var newMessage = $"{user}: {message}";
MessagesLB.Items.Add(newMessage);
});
});
try
{
await connection.StartAsync();
MessagesLB.Items.Add("Connected!");
}
catch(Exception ex)
{
MessagesLB.Items.Add(ex.Message);
}
}
What I do not understand:
When connecting the Client, how do I pass username and password from the winform to the Blazor server, and how to I get access to the middleware authentication and return a JTW back to the client connection. Do I need to make an API on the Blazor Server, do a call and generate a JWT which I pass here: options.AccessTokenProvider = () => Task.FromResult(token); or is there a more logical way?
I've looked into the AuthenticationStateProvider, but couldn't produce the results I wanted.
Is JWT authentication even intended for this, or is there a better alternative?
I'm using an already existing database filled with users that I'm going to access when this server is looking up if the username and password is correct. Meaning I want a "userService" or something like that which contains all users. For now it's fine to mock users, but I need to know where I can do this and swap it out with a DBconnection/context later on.
Any help would be most appreciated!
EDIT:
After some time I found a approach that did work as I wanted it. By using a custom Authentication scheme I could add a token containing user data from and to user/group which means I have complete control over the communication flow. A great benefit of this is that you never need to have a user database to check the authenticity of the user it self, I solved this by having baked into the token a salt/token value that only me and whom ever other person would integrate with the signalR relay server knows about. The Token then contains licensenumber, department no, user info etc to make it unique when registering.
SERVER SIDE:
public class MyAuthenticationHandler : AuthenticationHandler<MyCustomAuthenticationSchemeOptions>
{
public MyAuthenticationHandler(IOptionsMonitor<MyCustomAuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
TokenModel tokenModel;
if (!Request.Headers.ContainsKey(HeaderNames.Authorization))
{
return Task.FromResult(AuthenticateResult.Fail("Header Not Found."));
}
AuthenticationHeaderValue auth;
if (!AuthenticationHeaderValue.TryParse(Request.Headers.Authorization, out auth))
{
return Task.FromResult(AuthenticateResult.Fail("No authentication header"));
}
if (!auth.Scheme.Equals("Bearer"))
{
return Task.FromResult(AuthenticateResult.Fail("Authentication Scheme was not Bearer"));
}
//var header = Request.Headers[HeaderNames.Authorization].ToString();
//var tokenMatch = Regex.Match(header, MyAuthenticationSchemeConstants.MyToken);
if (!string.IsNullOrWhiteSpace(auth.Parameter))
{
//string[] token = header.Split(" ");
try
{
string parsedToken = Encoding.UTF8.GetString(Convert.FromBase64String(auth.Parameter));
tokenModel = JsonConvert.DeserializeObject<TokenModel>(parsedToken);
}
catch(Exception ex)
{
Debug.WriteLine("Exception Occured while Deserializing: " + ex);
return Task.FromResult(AuthenticateResult.Fail("TokenParseException"));
}
if(tokenModel != null)
{
List<Claim> claims = new()
{
new Claim(ClaimTypes.Name, tokenModel.Name),
new Claim("Group", tokenModel?.GroupName)
};
if (tokenModel.UserId > 0)
{
claims.Add(new Claim(ClaimTypes.NameIdentifier, tokenModel.UserId.ToString()));
}
var claimsIdentity = new ClaimsIdentity(claims,
nameof(MyAuthenticationHandler));
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(claimsIdentity), this.Scheme.Name);
// pass on the ticket to the middleware
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
return Task.FromResult(AuthenticateResult.Fail("Model is Empty"));
}
}
public class MyCustomAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
}
I also use a UserSerivice (though not necessary) to relay some information like "user not connected", a work in progress. Just a addUser, getID, removeUser from a list of usermodel. This enables me to have the hub like this:
public class ChatHub : Hub
{
IUserService _userService;
public ChatHub(IUserService userService)
{
_userService = userService;
}
public async Task SendMessageAsync(int user, string message)
{
Debug.WriteLine(Context.UserIdentifier); //userID -> Claim.UserIdentifier
bool test1 = Context.User.Identity.IsAuthenticated; //true
string test2 = Context?.User?.Identity?.Name; //myUserName -> Claim.Name
if (_userService.GetById(user) == false)
await Clients.User(Context.UserIdentifier).SendAsync("ReceiveMessage", Context?.User?.Identity?.Name, $"user is not connected");
await Clients.User(user.ToString()).SendAsync("ReceiveMessage", Context?.User?.Identity?.Name, message);
}
public async Task SendMessageToGroupAsync(string groupname, string message)
{
await Clients.OthersInGroup(groupname).SendAsync("ReceiveMessage", groupname, message);
}
public async override Task OnConnectedAsync()
{
//TODO Register Context.UserIdentifier -
int conID = 0;
if(int.TryParse(Context.UserIdentifier, out conID) == true)
{
_userService.AddUser(conID, Context.ConnectionId);
}
await Groups.AddToGroupAsync(Context.ConnectionId, Context.User.Claims.First(c => c.Type == "Group").Value);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
int conID = 0;
if (int.TryParse(Context.ConnectionId, out conID) == true)
{
_userService.RemoveUser(conID, Context.ConnectionId); //TODO Should probably be async calls to service.
}
await base.OnDisconnectedAsync(exception); //According to https://github.com/dotnet/aspnetcore/issues/19043 users are removed from all groups by disconnecting. Test/resolve this.
}
}
Then in the program.cs you just add it like usual (the MyAuthenticationSchemeConstants are just the name and a random token value I've chosen for now) :
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
// Add services to the container.
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddAuthentication(options => options.DefaultAuthenticateScheme = MyAuthenticationSchemeConstants.MyAuthenticationSchemeName)
.AddScheme<MyCustomAuthenticationSchemeOptions, MyAuthenticationHandler>( MyAuthenticationSchemeConstants.MyAuthenticationSchemeName, options => { });
UserService _userService = new();
.
.
.
app.UseAuthentication();
app.UseAuthorization();
app.MapBlazorHub();
app.MapHub<ChatHub>("/hubs/chathub");
Client-Side example:
Just a winform with a couple of text fields and buttons using localhost for example purposes, but just swap out the url for a central hub url.
string url = "https://localhost:7185/hubs/chathub";
string token = "";
public Form1()
{
InitializeComponent();
SendBtn.Enabled = false;
//client = new(baseAddress);
}
private void BuildToken()
{
TokenModel tokenModel = new()
{
UserId = Int32.Parse(UserIDTB.Text),
Name = UsernameTB.Text,
GroupName = "Waiter",
EmailAddress = "someEmail#mydomain.com"
};
token = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(tokenModel)));
}
private async void HubConnectBtn_Click(object sender, EventArgs e)
{
BuildToken();
connection = new HubConnectionBuilder()
.WithUrl(url, options =>
{
options.AccessTokenProvider = () => Task.FromResult(token);
})
.WithAutomaticReconnect()
.Build();
connection.On<string, string>("ReceiveMessage", (user, message) =>
{
this.Invoke(() =>
{
var newMessage = $"{user}: {message}";
MessagesLB.Items.Add(newMessage);
});
});
try
{
await connection.StartAsync();
SendBtn.Enabled = true;
HubConnectBtn.Enabled = false;
MessagesLB.Items.Add("Connected!");
}
catch (Exception ex)
{
MessagesLB.Items.Add(ex.Message);
}
}
private async void SendBtn_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(RecieverTB.Text))
{
MessagesLB.Items.Add("Write a recipient");
return;
}
if (string.IsNullOrWhiteSpace(MessageTB.Text))
{
MessagesLB.Items.Add("Write a message");
return;
}
try
{
await connection.InvokeAsync("SendMessageAsync", Int32.Parse(RecieverTB.Text), MessageTB.Text);
}
catch (Exception ex)
{
MessagesLB.Items.Add(ex.Message);
}
}
I hope this can help someone else!
Side note - if anyone has an input on how I can pass the Task.FromResult text to my clients that would be great!
I use signalr and jwt and my startup is a little different:
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = Configuration["JWT: ValidAudience"],
ValidIssuer = Configuration["JWT: ValidIssuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT: SecretKey"]))
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/api/hubs/")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
As you see you just have to set context.Token. But the token that signalr sends is obtained from the api through the authentication process. As you said, you authenticate sending a post request to a route on your api which could be something like this:
[HttpPost("authenticate")]
public async Task<ActionResult<AuthenticateResponse>> Post([FromBody]
AuthenticateRequest request)
{
AuthenticateResponse response = new AuthenticateResponse();
var result = await signInManager.PasswordSignInAsync(request.User,
request.Password, true, false);
if (result.Succeeded)
{
var user = await userManager.FindByNameAsync(request.User);
string[] roles = (await userManager.GetRolesAsync(user)).ToArray();
response.UserId = user.Id;
response.UserName = user.UserName;
response.Roles = roles;
response.Token = Helpers.TokenService.CreateToken(user, roles,
configuration);
}
else response.Error = "Authentication Error";
return Ok(response);
}
Here is the TokenService:
public static class TokenService
{
private const double EXPIRE_HOURS = 10;
public static string CreateToken(ApplicationUser user, string[] roles,
IConfiguration configuration)
{
var authClaims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id)
};
foreach (var userRole in roles)
{
authClaims.Add(new Claim(ClaimTypes.Role, userRole));
}
var authSigningKey = new
SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWT:
SecretKey"]));
var token = new JwtSecurityToken(
issuer: configuration["JWT: ValidIssuer"],
audience: configuration["JWT: ValidAudience"],
expires: DateTime.Now.AddHours(EXPIRE_HOURS),
claims: authClaims,
signingCredentials: new SigningCredentials(authSigningKey,
SecurityAlgorithms.HmacSha256)
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
And you have to derive your ApplicationdbContext from IdentityDbContext

ASP.NET Core 6 MVC Integration Tests - Authorization

Here is my problem. I can't seem to manage to create an integration test that requires an authenticated user. I use Microsoft.AspNetCore.Mvc.Testing for testing. Here is my test:
As seen the client has the role "Patient" and a UserId. Here are my helpers:
public class TestClaimsProvider
{
public IList<Claim> Claims { get; }
public TestClaimsProvider(IList<Claim> claims)
{
Claims = claims;
}
public TestClaimsProvider()
{
Claims = new List<Claim>();
}
public static TestClaimsProvider WithAdminClaims()
{
var provider = new TestClaimsProvider();
provider.Claims.Add(new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString()));
provider.Claims.Add(new Claim(ClaimTypes.Name, "Admin user"));
provider.Claims.Add(new Claim(ClaimTypes.Role, "Administrator"));
return provider;
}
public static TestClaimsProvider WithUserClaims()
{
var provider = new TestClaimsProvider();
provider.Claims.Add(new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString()));
provider.Claims.Add(new Claim(ClaimTypes.Name, "Patient"));
provider.Claims.Add(new Claim(ClaimTypes.Role, "Patient"));
return provider;
}
}
This also:
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly IList<Claim> _claims;
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock, TestClaimsProvider claimsProvider) : base(options, logger, encoder, clock)
{
_claims = claimsProvider.Claims;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var identity = new ClaimsIdentity(_claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "Test");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
And:
public static class WebApplicationFactoryExtensions
{
public static WebApplicationFactory<T> WithAuthentication<T>(this WebApplicationFactory<T> factory, TestClaimsProvider claimsProvider) where T : class
{
return factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication("Test")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("Test", op => { });
services.AddScoped<TestClaimsProvider>(_ => claimsProvider);
});
});
}
public static HttpClient CreateClientWithTestAuth<T>(this WebApplicationFactory<T> factory, TestClaimsProvider claimsProvider) where T : class
{
var client = factory.WithAuthentication(claimsProvider).CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Test");
return client;
}
These are based on this subject https://gunnarpeipman.com/aspnet-core-integration-tests-users-roles/. Although I am not using the FakeStartup class that he has pointed in previous threads. Also I have tried the authentication from the docs here https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-6.0#customize-the-client-with-withwebhostbuilder but it is the same.
Here is my action method in the controller:
[Authorize(Roles = PatientRoleName)]
public async Task<IActionResult> MakePatientAppointment()
{
var patient = await this.patientService.GetPatientByUserIdAsync(this.User.GetId());
if (string.IsNullOrWhiteSpace(patient.FirstName) ||
string.IsNullOrWhiteSpace(patient.LastName) ||
string.IsNullOrWhiteSpace(patient.Phone))
{
this.TempData["Message"] = PatientProfileIsNotFinishedMsg;
return RedirectToAction("Finish", "Patient", new { area = "" });
}
var viewModel = new PatientAppointmentCreateModel
{
DoctorId = await this.doctorService.GetDoctorId(),
AppointmentCauses = await this.appointmentCauseService.GetAllCauses()
};
return View(viewModel);
}
From debugging the test the response redirect is to /Identity/Login, so from what I am understanding the user is not logged in. How can I refactor the code to manage to get the user authenticated?
Updated for ASP.net 6
The top-level statements new feature of C# 10, provided the unification of the Startup and Program classes into a single Program class, and because of that (among other things like the new minimal hosting model), the creation of the WebApplicationFactory has changed, starting with the way to change the visibility of the Program class to the TestFixture (since it now doesn't contain a namespace due to the top-level statements feature)...
And the documentation has been updated as such, however, the way we registered the AuthenticationHandler to create an impersonated client for authorization has also changed - but the documentation (at least so far) doesn't address this.
So if we register the AuthenticationHandler as we did until ASP.net 5, now in ASP.net 6, we get a 401 Unauthorized (because it's not working anymore).
After a lot of research I found the solution:
Now, it's necessary to make explicit the AuthenticationOptions settings in the WebApplicationFactory according to the settings you have in your WebApi, in my case:
{
o.DefaultAuthenticateScheme = "Test";
o.DefaultChallengeScheme = "Test";
}

.net 5 how to setup authorization using custom provider

in my .net 5 website i have to read user login from header and the call external webservice to check if is authorized and get permission list.
EDIT 3:
GOALS
Read current user from http header setted by corporate single sign-on
Read user permission and info by calling external web services and
keep them daved to prevent extra-calls for every action
let the user be free to access by any page
authorize by default all controller's actions with custom claims
Actual Problem
context.User.Identity.IsAuthenticated in middleware is always false
Actual code
Startup - ConfigureServices
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
services.AddControllers(options => { options.Filters.Add<AuditAuthorizationFilter>(); });
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
Startup - Configure
app.UseMiddleware<AuthenticationMiddleware>();
app.UseAuthentication();
app.UseAuthorization();
Middleware
public class AuthenticationMiddleware
{
private readonly RequestDelegate _next;
// Dependency Injection
public AuthenticationMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
if (!context.User.Identity.IsAuthenticated)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, context.Request.Headers["Token"]),
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaultsAuthenticationScheme);
var authProperties = new AuthenticationProperties();
await context.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
}
await _next(context);
}
}
Filter
public class AuditAuthorizationFilter : IAuthorizationFilter, IOrderedFilter
{
public int Order => -1;
private readonly IHttpContextAccessor _httpContextAccessor;
public AuditAuthorizationFilter(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
if (context.HttpContext.User.Identity.IsAuthenticated)
{
context.Result = new ForbidResult();
}
else
{
string metodo = $"{context.RouteData.Values["controller"]}/{context.RouteData.Values["action"]}";
if (!context.HttpContext.User.HasClaim("type", metodo))
{
context.Result = new ForbidResult();
}
}
}
}
EDIT 2:
my Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddDevExpressControls();
services.AddTransient<ILoggingService, LoggingService>();
services.AddHttpContextAccessor();
services.AddMvc().SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Version_3_0);
services.ConfigureReportingServices(configurator => {
configurator.UseAsyncEngine();
configurator.ConfigureWebDocumentViewer(viewerConfigurator => {
viewerConfigurator.UseCachedReportSourceBuilder();
});
});
services.AddControllersWithViews().AddJsonOptions(options => options.JsonSerializerOptions.PropertyNamingPolicy = null);
services.AddControllersWithViews().AddRazorRuntimeCompilation();
services.AddControllers(options => { options.Filters.Add(new MyAuthenticationAttribute ()); });
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
app.UseDevExpressControls();
app.UseExceptionHandlerMiddleware(Log.Logger, errorPagePath: "/Error/HandleError" , respondWithJsonErrorDetails: true);
app.UseStatusCodePagesWithReExecute("/Error/HandleError/{0}");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSerilogRequestLogging(opts => opts.EnrichDiagnosticContext = LogHelper.EnrichFromRequest);
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
EDIT 1:
to adapt original code to .net 5 i made some changes:
if (!context.HttpContext.User.Identity.IsAuthenticated)
{
const string MyHeaderToken = "HTTP_KEY";
string userSSO = null;
if (string.IsNullOrWhiteSpace(context.HttpContext.Request.Headers[MyHeaderToken]))
{
userSSO = context.HttpContext.Request.Headers[MyHeaderToken];
}
if (string.IsNullOrWhiteSpace(userSSO))
{
//filterContext.Result = new unh();
}
else
{
// Create GenericPrincipal
GenericIdentity webIdentity = new GenericIdentity(userSSO, "My");
//string[] methods = new string[0]; // GetMethods(userSSO);
GenericPrincipal principal = new GenericPrincipal(webIdentity, null);
IdentityUser user = new (userSSO);
Thread.CurrentPrincipal = principal;
}
}
but context.HttpContext.User.Identity.IsAuthenticated is false everytimes, even if the previous action set principal
ORIGINAL:
I'using custom attribute to manage this scenario in this way:
public class MyAuthenticationAttribute : ActionFilterAttribute, IAuthenticationFilter{
public string[] Roles { get; set; }
public void OnAuthentication(AuthenticationContext filterContext)
{
string MyHeaderToken = “SM_USER”;
string userSSO = null;
if (HttpContext.Current.Request.Headers[MyHeaderToken] != null)
{
userSSO = HttpContext.Current.Request.Headers[MyHeaderToken];
Trace.WriteLine(string.Format(“got MyToken: {0}”, userSSO));
}
if (string.IsNullOrWhiteSpace(userSSO))
{
Trace.WriteLine(“access denied, no token found”);
}
else
{
// Create GenericPrincipal
GenericIdentity webIdentity = new GenericIdentity(userSSO, “My”);
string[] methods= GetMethods(userSSO);
GenericPrincipal principal = new GenericPrincipal(webIdentity, methods);
filterContext.HttpContext.User = principal;
}
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
{
//check authorizations
}
}
but external webservice returns list of controller/action authorized for users, so i have to test all actions executions to simply check if names is contained in the list.
is there a way to do this without have to write attribute on every actions or every controllers in this way:
[MyAuthentication(Roles = “Admin”)]
pubic class AdminController: Controller
{
}
i know i can use
services.AddMvc(o =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
o.Filters.Add(new AuthorizeFilter(policy));
});
but no idea of how to use this with my custom authorization
i'am also not sure if string[] methods= GetMethods(userSSO) is cached by .net core filterContext.HttpContext.User avoiding multiple calls to external webservice.
Thanks
If you want to apply your custom IAuthenticationFilter globally then you can do the ff:
services.AddControllers(options =>
{
options.Filters.Add(new MyAuthenticationFilter());
});
With this approach, you no longer need to inherit from ActionFilterAttribute and no need to add the [MyAuthentication(Roles = “Admin”)] attributes.
Just ensure that you are allowing anonymous requests to actions that doesn't need authentication and/or authorization.
EDIT 2:
For your updated setup, make sure you do the ff:
Add cookie authentication
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie();
Order of middlewares
app.UseRouting();
app.UseAuthentication();
app.UseMiddleware<AuthenticationMiddleware>();
app.UseAuthorization();
EDIT 1:
i'am also not sure if string[] methods= GetMethods(userSSO) is cached by .net core filterContext.HttpContext.User avoiding multiple calls to external webservice.
The lifetime of the filter depends on how you implemented it, usually it is singleton but you can make it transient by following the approach below:
public class MyAuthorizationFilter : IAuthorizationFilter, IOrderedFilter
{
public int Order => -1; // Ensures that it runs first before basic Authorize filter
public void OnAuthorization(AuthorizationFilterContext context)
{
if (!context.HttpContext.User.Identity.IsAuthenticated)
{
if (context.HttpContext.Session.IsAvailable
&& context.HttpContext.Session.TryGetValue("_SessionUser", out byte[] _user))
{
SessionUser su = (SessionUser)this.ByteArrayToObject(_user);
GenericPrincipal principal = this.CreateGenericPrincipal(su.IdentityName, su.Type, su.Roles);
context.HttpContext.User = principal;
}
else
{
const string MyHeaderToken = "HTTP_KEY";
string userSSO = null;
if (!string.IsNullOrWhiteSpace(context.HttpContext.Request.Headers[MyHeaderToken]))
{
userSSO = context.HttpContext.Request.Headers[MyHeaderToken];
}
userSSO = "TestUser";
if (string.IsNullOrWhiteSpace(userSSO))
{
//filterContext.Result = new unh();
}
else
{
string identityType = "My";
string[] methods = new string[0]; // GetMethods(userSSO);
// Create GenericPrincipal
GenericPrincipal principal = this.CreateGenericPrincipal(userSSO, identityType, methods);
context.HttpContext.User = principal;
if (context.HttpContext.Session.IsAvailable)
{
SessionUser su = new SessionUser()
{
IdentityName = principal.Identity.Name,
Type = principal.Identity.AuthenticationType,
Roles = methods
};
byte[] _sessionUser = this.ObjectToByteArray(su);
context.HttpContext.Session.Set("_SessionUser", _sessionUser);
}
}
}
}
}
private GenericPrincipal CreateGenericPrincipal(string name, string type, string[] roles)
{
GenericIdentity webIdentity = new GenericIdentity(name, type);
GenericPrincipal principal = new GenericPrincipal(webIdentity, roles);
return principal;
}
// Convert an object to a byte array
private byte[] ObjectToByteArray(Object obj)
{
BinaryFormatter bf = new BinaryFormatter();
using (var ms = new MemoryStream())
{
bf.Serialize(ms, obj);
return ms.ToArray();
}
}
// Convert a byte array to an Object
private Object ByteArrayToObject(byte[] arrBytes)
{
using (var memStream = new MemoryStream())
{
var binForm = new BinaryFormatter();
memStream.Write(arrBytes, 0, arrBytes.Length);
memStream.Seek(0, SeekOrigin.Begin);
var obj = binForm.Deserialize(memStream);
return obj;
}
}
[Serializable]
private class SessionUser
{
public string IdentityName { get; set; }
public string Type { get; set; }
public string[] Roles { get; set; }
}
}
public class MyAuthorizationAttribute : TypeFilterAttribute
{
public MyAuthorizationAttribute()
: base(typeof(MyAuthorizationFilter))
{
}
}
On Startup.cs > Configure call app.UseSession(); immediately after app.UseRouting() so that session will be available during authorization.
The code above will set the current HTTP Context's user and save it on session. Subsequent requests will attempt to use the user stored on the session. This will also make the DI container manage the lifetime of the filter. Read more about it in Filters in ASP.NET Core.
I do not recommend you follow this approach. Please do either cookie or token-based authentication by taking advantage of the authentication middleware in .NET Core.
Once the request reaches the action execution, context.HttpContext.User.Identity.IsAuthenticated will now be true.

Skip JWT Auth during Tests ASP.Net Core 3.1 Web Api

I a have a very simple app with one JWT authenticated controller:
[ApiController]
[Authorize]
[Route("[controller]")]
public class JwtController : ControllerBase
{
public JwtController() { }
[HttpGet]
public ActionResult Get() => Ok("Working!");
}
With the authentication configured as:
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false
};
});
During tests, i want the user to be "authenticated" all the time so that [Authorize] would be skipped.
[Fact]
public async Task JwtIsSkipped()
{
var response = (await _Client.GetAsync("/jwt")).EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Equal("Working!", stringResponse);
}
Running the test like this will fail, so following this doc I added this simple auth handler:
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public const string DefaultScheme = "Test";
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, DefaultScheme);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, DefaultScheme);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
So now my test class looks like this:
public class UnitTest : IClassFixture<WebApplicationFactory<Startup>>
{
private readonly WebApplicationFactory<Startup> _Factory;
private readonly HttpClient _Client;
public UnitTest(WebApplicationFactory<Startup> factory)
{
_Factory = factory;
_Client = _Factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication(TestAuthHandler.DefaultScheme)
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
TestAuthHandler.DefaultScheme, options => { });
});
}).CreateClient();
_Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.DefaultScheme);
}
[Fact]
public async Task JwtIsSkipped()
{
var response = (await _Client.GetAsync("/jwt")).EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Equal("Working!", stringResponse);
}
}
And it still fails, I have no idea what I'm doing wrong.
I have had a similar situation previously with the Microsoft example and can promise you it can give headaches, it may work on specific Core versions, but I have given up. I have solved this way.
My goal was, is to Authorize the system while testing, instead of using AddAuthentication in our test we create a FakePolicyEvaluator class and add it as a singleton to our test.
So let's go to our FakePolicyEvaluator class:
public class FakePolicyEvaluator : IPolicyEvaluator
{
public virtual async Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context)
{
var principal = new ClaimsPrincipal();
principal.AddIdentity(new ClaimsIdentity(new[] {
new Claim("Permission", "CanViewPage"),
new Claim("Manager", "yes"),
new Claim(ClaimTypes.Role, "Administrator"),
new Claim(ClaimTypes.NameIdentifier, "John")
}, "FakeScheme"));
return await Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal,
new AuthenticationProperties(), "FakeScheme")));
}
public virtual async Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy,
AuthenticateResult authenticationResult, HttpContext context, object resource)
{
return await Task.FromResult(PolicyAuthorizationResult.Success());
}
}
Then in our ConfigureTestServices we added services.AddSingleton<IPolicyEvaluator, FakePolicyEvaluator>();
So in your test code like this:
private readonly HttpClient _client;
public UnitTest(WebApplicationFactory<Startup> factory)
{
_client = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddSingleton<IPolicyEvaluator, FakePolicyEvaluator>();
});
}).CreateClient();
}
[Fact]
public async Task JwtIsSkipped()
{
var response = (await _client.GetAsync("/jwt")).EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Equal("Working!", stringResponse);
}
That is it. Now when you test, it will bypass authentication. I have tested it with the provided controller and it works.
It is also possible to place the fake inside the application startup, and it will be both testable for test and working under a development environment. Check the referenced article.
Disclaimer: I have written in more depth article about this on my personal website Reference where you can find and download a source code from GitHub.
You need to set DefaultAuthenticateScheme
builder.ConfigureTestServices(services =>
{
services.AddAuthentication(options =>
{
x.DefaultAuthenticateScheme = TestAuthHandler.DefaultScheme;
x.DefaultScheme = TestAuthHandler.DefaultScheme;
}).AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
TestAuthHandler.DefaultScheme, options => { });
});
its a small change to maysam fahmi answer that HttpContext.User also have values:
public class FakeUserPolicyEvaluator: IPolicyEvaluator
{
private ClaimsIdentity _claimsIdentity;
public virtual async Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context)
{
var testScheme = "FakeScheme";
var principal = new ClaimsPrincipal();
_claimsIdentity = new ClaimsIdentity(new[]
{
new Claim("sub", "a5"),
new Claim("client_id", "a6"),
new Claim(ClaimTypes.Role, BackmanConsts.Authorization.ClientPolicy),
}, testScheme);
principal.AddIdentity(_claimsIdentity);
return await Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal,
new AuthenticationProperties(), testScheme)));
}
public virtual async Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy,
AuthenticateResult authenticationResult, HttpContext context, object resource)
{
context.User = new ClaimsPrincipal(_claimsIdentity);
return await Task.FromResult(PolicyAuthorizationResult.Success());
}
}

Categories

Resources