Can I avoid using BuildServiceProvider in ExpireToken function below? In other words...
Is there a way of avoiding BuildServiceProvider() in Startup.cs when using JWTBearer OnAuthenticationFailed event?
(Using Web API aspnet Core 3.0)
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
//some code removed..
//Authentication
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = context =>
{
if (context.Exception.Message.Contains("The token is expired"))
{
Microsoft.Extensions.Primitives.StringValues token = string.Empty;
var logger = new LoggerManager();
logger.LogError("Token expired"); //confirming that the token expired so authentication failed
var header = context.Request.Headers;
if (!header.TryGetValue("Authorization", out token))
logger.LogError("no token found");
else
token = token.ToString().Substring("Bearer ".Length).Trim();
ExpireToken(services, token);
}
return Task.CompletedTask;
}
};
});
}
public void ExpireToken(IServiceCollection services, string tokenId)
{
var sp = services.BuildServiceProvider();
var jwtManager = sp.GetService<IJWTAuthenticationManager>();
jwtManager.ExpireToken(tokenId);
}
jwtAuthentication Manager will publish this event so that subscribers are notified (c#:event + delegates)
public delegate void TokenExpiredEventHandler(object source, TokenExpiredEventArgs args);
JWTAuthenticationManager.cs
public class JWTAuthenticationManager : IJWTAuthenticationManager
{
private readonly string _tokenKey;
public event TokenExpiredEventHandler TokenExpired;
//some code removed for brevity
public void ExpireToken(string tokenId)
{
OnTokenExpired(tokenId); //notify all subscribers
}
protected virtual void OnTokenExpired(string token) {
TokenExpired?.Invoke(this, new TokenExpiredEventArgs(token));
}
}
public class TokenExpiredEventArgs : EventArgs
{
public string token;
public TokenExpiredEventArgs(string tokenId)
{
token = tokenId;
}
}
Program.cs
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
var jwtMgr = host.Services.GetRequiredService<IJWTAuthenticationManager>();
var cacheMgr = host.Services.GetRequiredService<ICacheManager>();
jwtMgr.TokenExpired += cacheMgr.OnTokenExpired;
host.Run();
}
Why not resolve services from context? Simply change your ExpireToken method to take an IServiceProvider:
public void ExpireToken(IServiceProvider services, string tokenId)
{
var jwtManager = services.GetService<IJWTAuthenticationManager>();
jwtManager.ExpireToken(tokenId);
}
And then pass context.HttpContext.RequestServices:
ExpireToken(context.HttpContext.RequestServices, token);
Related
I'm having a hard time to use JwtBearerEvents with my custom ActionFilter that I use to authenticate users using JWT token by calling a class that contain the token validation logic.
The intention of using JwtBearerEvents is to log every attempt of logging whether successful or failed. Is it possible in my case to use JwtBearerEvents? because all documentation says that JwtBearerEvents is usually used with Authorize attribute.
I created a custom JwtBearerEvents class to log and configure it in the startup file but this class has never been triggered during authentication. any thoughts please? Thank you for your help. this is a sample code of what I have currently:
public interface IAuthenticationService
{
public bool ValidateTokenOrThrow();
}
public interface AuthenticationService
{
public bool ValidateTokenOrThrow()
{
//logic of token validation where i use SecurityTokenHandler
//....
}
}
public class CustomActionAttribute : ActionFilterAttribute
{
private readonly IAuthenticationService _authenticationService { get; set; }
public CustomActionAttribute(IAuthenticationService authenticationService)
{
_authenticationService = authenticationService;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
var token = context.HttpContext.Request.Headers["Authorization"];
var isValidToken = _authenticationService.ValidateTokenOrThrow(token); // this is an error as both parameters do not exist at this moment
if (!isValidToken)
context.Result = new ForbidResult();
}
}
public CustomJwtBearerEvents : JwtBearerEvents
{
private readonly ILogger<JwtBearerEvents>_logger;
public CustomJwtBearerEvents(ILogger<JwtBearerEvents> logger)
{
_logger = logger;
}
public override Task OnAuthenticationFailed = context =>
{
context.Response.StatusCode = HttpStatusCodes.AuthenticationFailed;
context.Response.ContentType = "application/json";
var err = this.Environment.IsDevelopment() ? context.Exception.ToString() : "An error occurred processing your authentication.";
var result = JsonConvert.SerializeObject(new {err});
return context.Response.WriteAsync(result);
}
}
//Startup file
public void ConfigureServices(IServiceCollection services)
{
service.AddSingleton<CustomJwtBearerEvents>();
services.AddAuthorization();
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.EventType = typeof(CustomJwtBearerEvents);
});
}
public HomeController : BaseController
{
[CustomActionAttribute]
[Route("api/[controller]")]
public GetHome()
{
return service.GetHome();
}
}
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
I try to do integrations tests with WebApplicationFactory, but I get error.
Part of Program.cs:
builder.AddNegotiate(options =>
{
var ldapConnectionsFactory = new LdapConnectionsFactory(domainConfiguration, loggerFactory.CreateLogger<LdapConnectionsFactory>());
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && ldapConnectionsFactory.TryCreate(out var ldapConnection))
{
options.EnableLdap(settings =>
{
settings.LdapConnection = ldapConnection;
settings.Domain = domainConfiguration.Domain;
});
}
});
This code add six services to services array. If I don't add builder.AddNegotiate, I don't have problem.
When I try to do test, I get error:
Negotiate authentication requires a server that supports IConnectionItemsFeature like Kestrel.
TestFile:
[TestFixture]
public class AuthControllerTests
{
readonly CustomWebApplicationFactory<Program> _factory;
private readonly HttpClient _client;
public AuthControllerTests()
{
_factory = new CustomWebApplicationFactory<Program>();
_client = _factory.CreateClient();
_client.BaseAddress = new Uri("http://localhost:8001/");
}
[Test]
public async Task CheckAdminLogIn_SendRequest_ShouldReturnOk()
{
// Arrange
var credentials = new Credentials() { Login = "admin", Password = "admin" };
var jsonSerializerOptions = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNameCaseInsensitive = true
};
string jsonString = JsonSerializer.Serialize(credentials, jsonSerializerOptions);
StringContent httpContent = new StringContent(jsonString, System.Text.Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("api/Auth/login", httpContent);
// Assert
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
}
I try remove services in CustomWebApplicationFactory:
public class CustomWebApplicationFactory<TStartup>
: WebApplicationFactory<TStartup> where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Here I am trying to find and delete Negotiate, but it deletes 4 out of 6 services
var negotiateHandler = services.SingleOrDefault(d => d.ServiceType == typeof(NegotiateHandler));
services.Remove(negotiateHandler);
List<ServiceDescriptor> servicesForRemove = services.Where(d => d.ServiceType.FullName.Contains("Negotiate")).ToList();
foreach (var s in servicesForRemove)
{
services.Remove(s);
}
});
}
}
It is not removed:
Is there another way to disable negotiate auth?
I removed all Microsoft.AspNetCore.Authentication, then added JwtBearer. It works for me
public class CustomWebApplicationFactory<TStartup>
: WebApplicationFactory<TStartup> where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
List<ServiceDescriptor> servicesForRemove = services.Where(d => d.ServiceType.FullName.Contains("Microsoft.AspNetCore.Authentication")).ToList();
foreach (var s in servicesForRemove)
{
services.Remove(s);
}
var buildServiceProvider = services.BuildServiceProvider();
var loggerFactory = buildServiceProvider.GetService<ILoggerFactory>();
services.AddAuthentication().AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = JwtProvider.CreateTokenValidator(loggerFactory);
});
});
}
}
You can remove a specific scheme with:
internal class CustomWebApplicationFactory : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
services.Configure<AuthenticationOptions>(o =>
{
if (o.Schemes is List<AuthenticationSchemeBuilder> schemes)
{
schemes.RemoveAll(s => s.Name == NegotiateDefaults.AuthenticationScheme);
o.SchemeMap.Remove(NegotiateDefaults.AuthenticationScheme);
}
});
});
}
}
When I run my app, I keep getting the following: ConnectionID required
So I can't seem to be able to connect my ASP.NET Core API server to my asp.net uwp client.
I have installed the Microsoft.AspNetCore.SignalR 1.1.0 nuget package on my server side and the Microsoft.AspNet.SignalR.Client 2.4.1 nuget package on my client side.
This is my Startup.cs class:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("FlightAppContext"));
});
services.AddSession();
services.AddOpenApiDocument(c =>
{
c.DocumentName = "apidocs";
c.Title = "FlightApp API";
c.Version = "v1";
c.Description = "The FlightApp API documentation description.";
c.DocumentProcessors.Add(new SecurityDefinitionAppender("JWT Token", new SwaggerSecurityScheme
{
Type = SwaggerSecuritySchemeType.ApiKey,
Name = "Authorization",
In = SwaggerSecurityApiKeyLocation.Header,
Description = "Copy 'Bearer' + valid JWT token into field"
}));
c.OperationProcessors.Add(new OperationSecurityScopeProcessor("JWT Token"));
}); //for OpenAPI 3.0 else AddSwaggerDocument();
services.AddSignalR();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddMvc().AddXmlSerializerFormatters();
services.AddScoped<IEntertainmentRepository, EntertainmentRepository>();
services.AddScoped<IOrderlineRepository, OrderlineRepository>();
services.AddScoped<IPassengerRepository, PassengerRepository>();
services.AddScoped<IPersonnelRepository, PersonnelRepository>();
services.AddScoped<IProductRepository, ProductRepository>();
services.AddScoped<IPassengerGroupRepository, PassengerGroupRepository>();
services.AddScoped<ApplicationDataInitializer>();
services.AddCors(options => options.AddPolicy("AllowAllOrigins", builder => builder.AllowAnyOrigin()));
}
//This method gets called by the runtime.Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ApplicationDataInitializer dataInitializer)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
//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.UseAuthentication();
//app.UseHttpsRedirection();
app.UseSignalR(route => route.MapHub<ChatHub>("/api/Chat/GetAllChatMessages"));
app.UseMvc();
app.UseSwaggerUi3();
app.UseSwagger();
dataInitializer.InitializeData();
}
}
This is my ChatHub.cs class:
public class ChatHub : Hub
{
public Task SendMessage(int PassengergroupId, string userName, string message, DateTime sendTime)
{
return Clients.All.SendAsync("ReceiveMessage", userName, message, sendTime);
//Groups.AddToGroupAsync(Context.ConnectionId, Convert.ToString(PassengergroupId));
//return Clients.Groups(Convert.ToString(PassengergroupId)).SendAsync("ReceiveMessage", userName, message, sendTime);
}
}
this is my ChatController:
/// <summary>
/// Adds a message to a passengroup
/// </summary>
/// <param name="message">message to post</param>
[HttpPost("ChatMessages")]
public async Task<IActionResult> CreateMessage(MessageDTO message)
{
PassengerGroup passengerGroup = _passengerGroupRepository.GetById(message.PassengergroupId);
Passenger currentUser = _passengerRepository.GetById(message.PassengerId);
Message messageToCreate = new Message(currentUser, passengerGroup, message.Content);
await _dbContext.Messages.AddAsync(messageToCreate);
await _dbContext.SaveChangesAsync();
return Ok();
}
/// <summary>
/// Get all messages from passengergroup
/// </summary>
[HttpGet("GetAllChatMessagesFromPassengerGroup/{id}")]
public async Task<IEnumerable<MessageDTO>> GetAllChatMessagesFromPassengerGroup(int id)
{
var messages = await _dbContext.Messages.Include(m => m.Passenger)
.Include(m=>m.PassengerGroup).Where(e => e.PassengroupId == id).ToListAsync();
ICollection<MessageDTO> messagesDTO = new List<MessageDTO>();
foreach (var m in messages)
{
MessageDTO message = new MessageDTO(m);
messagesDTO.Add(message);
}
return messagesDTO;
}
/// <summary>
/// Get all messages
/// </summary>
[HttpGet("GetAllChatMessages")]
public async Task<IEnumerable<MessageDTO>> GetAllChatMessages()
{
var messages = await _dbContext.Messages.Include(m => m.Passenger)
.Include(m => m.PassengerGroup).ToListAsync();
ICollection<MessageDTO> messagesDTO = new List<MessageDTO>();
foreach (var m in messages)
{
MessageDTO message = new MessageDTO(m);
messagesDTO.Add(message);
}
return messagesDTO;
}
}
and this is my Chat.xaml.cs class:
public sealed partial class Chat : Page
{
public HubConnection hubConnection;
public IHubProxy hubProxy;
public ObservableCollection<Message> MessagesList { get; set; }
private HttpClient client = new HttpClient();
public Chat()
{
InitializeComponent();
}
public int CurrentPassengerId;
private Passenger current;
protected override void OnNavigatedTo(NavigationEventArgs e)
{
var currentPassengerId = e.Parameter.ToString();
this.CurrentPassengerId = Int32.Parse(currentPassengerId);
base.OnNavigatedTo(e);
GetCurrentUser(CurrentPassengerId);
LoadMessages(current.PassengerGroupId);
InitilizeHub(current.PassengerGroupId);
}
private async void GetCurrentUser(int id)
{
var json = await client.GetStringAsync(new Uri($"http://localhost:53385/api/Passenger/{id}"));
current = JsonConvert.DeserializeObject<Passenger>(json);
this.userTextBlock.Text = current.FirstName + current.LastName;
}
private async void InitilizeHub(int id)
{
hubConnection = new HubConnection($"http://localhost:53385/api/Chat/GetAllChatMessages");
hubProxy = hubConnection.CreateHubProxy("ChatHub");
hubProxy.On<string, string, DateTime>("receiveMessage", GetMessage);
await hubConnection.Start();
if (hubConnection.State != ConnectionState.Connected)
{ await hubConnection.Start(); }
}
private async void GetMessage(string userName, string message, DateTime sendTime)
{
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
this.MessageListbox.Items.Add($"{sendTime.ToString("HH:mm")}\n{userName}: {message}")
);
}
private async void SendButton_Click(object sender, RoutedEventArgs e)
{
await hubProxy?.Invoke("sendMessage", current.PassengerGroupId, userTextBlock.Text, messageTextBox.Text, DateTime.Now);
var message = new Message(messageTextBox.Text, userTextBlock.Text, current.PassengerGroupId);
var messageJson = JsonConvert.SerializeObject(message);
var res = await client.PostAsync("http://localhost:53385/api/Chat/ChatMessages",
new StringContent(messageJson, System.Text.Encoding.UTF8, "application/json"));
messageTextBox.Text = string.Empty;
}
public async void LoadMessages(int id)
{
var json = await client.GetStringAsync(new Uri($"http://localhost:53385/api/Chat/GetAllChatMessagesFromPassengerGroup/{id}"));
var lst = JsonConvert.DeserializeObject<ObservableCollection<Message>>(json);
foreach (var message in lst)
{
MessagesList.Add(message);
this.MessageListbox.Items.Add($"{message.TimeSend.ToString("HH:mm")}\n{message.Name}: {message.Content}");
}
}
}
I've built a simple AspNetCore 2.2 API that uses IdentityServer 4 to handle OAuth. It's working fine but I'd now like to add integration tests and recently discovered this. I used it to build some tests which all worked fine - as long as I didn't have the [Authorize] attribute on my controllers - but obviously that attribute needs to be there.
I came across this stackoverflow question and from the answers given there I tried to put a test together but I'm still getting an Unauthorized response when I try to run tests.
Please note: I really don't know what details I should be using when I'm creating the client.
What should the allowed scopes be? (Should they match the real
scopes)
Also when building the IdentityServerWebHostBuilder
What should I pass to .AddApiResources? (Maybe a dumb question but
does it matter)
If anyone can guide me it would be greatly appreciated.
Here is my test:
[Fact]
public async Task Attempt_To_Test_InMemory_IdentityServer()
{
// Create a client
var clientConfiguration = new ClientConfiguration("MyClient", "MySecret");
var client = new Client
{
ClientId = clientConfiguration.Id,
ClientSecrets = new List<Secret>
{
new Secret(clientConfiguration.Secret.Sha256())
},
AllowedScopes = new[] { "api1" },
AllowedGrantTypes = new[] { GrantType.ClientCredentials },
AccessTokenType = AccessTokenType.Jwt,
AllowOfflineAccess = true
};
var webHostBuilder = new IdentityServerWebHostBuilder()
.AddClients(client)
.AddApiResources(new ApiResource("api1", "api1name"))
.CreateWebHostBuilder();
var identityServerProxy = new IdentityServerProxy(webHostBuilder);
var tokenResponse = await identityServerProxy.GetClientAccessTokenAsync(clientConfiguration, "api1");
// *****
// Note: creating an IdentityServerProxy above in order to get an access token
// causes the next line to throw an exception stating: WebHostBuilder allows creation only of a single instance of WebHost
// *****
// Create an auth server from the IdentityServerWebHostBuilder
HttpMessageHandler handler;
try
{
var fakeAuthServer = new TestServer(webHostBuilder);
handler = fakeAuthServer.CreateHandler();
}
catch (Exception e)
{
throw;
}
// Create an auth server from the IdentityServerWebHostBuilder
HttpMessageHandler handler;
try
{
var fakeAuthServer = new TestServer(webHostBuilder);
handler = fakeAuthServer.CreateHandler();
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
// Set the BackChannelHandler of the 'production' IdentityServer to use the
// handler form the fakeAuthServer
Startup.BackChannelHandler = handler;
// Create the apiServer
var apiServer = new TestServer(new WebHostBuilder().UseStartup<Startup>());
var apiClient = apiServer.CreateClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
var user = new User
{
Username = "simonlomax#ekm.com",
Password = "Password-123"
};
var req = new HttpRequestMessage(new HttpMethod("GET"), "/api/users/login")
{
Content = new StringContent(JsonConvert.SerializeObject(user), Encoding.UTF8, "application/json"),
};
// Act
var response = await apiClient.SendAsync(req);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
My Startup class:
public class Startup
{
public IConfiguration Configuration { get; }
public static HttpMessageHandler BackChannelHandler { get; set; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
ConfigureAuth(services);
services.AddTransient<IPassportService, PassportService>();
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
}
protected virtual void ConfigureAuth(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = Configuration.GetValue<string>("IdentityServerAuthority");
options.Audience = Configuration.GetValue<string>("IdentityServerAudience");
options.BackchannelHttpHandler = BackChannelHandler;
});
}
// 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.UseAuthentication();
app.UseHttpsRedirection();
app.UseMvc();
app.UseExceptionMiddleware();
}
}
Edit:
The below suggestion was one problem. The original source-code failed due to an exception by trying to build WebHostBuilder twice. Secondly the configuration-file was only present in the API project, not in the test-project, thats why authority wasn't set as well.
Instead of doing this
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = Configuration.GetValue<string>("IdentityServerAuthority");
options.Audience = Configuration.GetValue<string>("IdentityServerAudience");
options.BackchannelHttpHandler = BackChannelHandler;
});
You have to do something like this:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = Configuration.GetValue<string>("IdentityServerAuthority");
options.JwtBackChannelHandler = BackChannelHandler;
});
You can find a sample here.
Hope that helps, worked for me!
A solution which doesn't affect production code:
public class TestApiWebApplicationFactory<TStartup>
: WebApplicationFactory<TStartup> where TStartup : class
{
private readonly HttpClient _identityServerClient;
public TestApiWebApplicationFactory(HttpClient identityServerClient)
{
_identityServerClient = identityServerClient;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);
builder.ConfigureServices(
s =>
{
s.AddSingleton<IConfigureOptions<JwtBearerOptions>>(services =>
{
return new TestJwtBearerOptions(_identityServerClient);
});
});
}
}
and its usage is:
_factory = new WebApplicationFactory<Startup>()
{
ClientOptions = {BaseAddress = new Uri("http://localhost:5000/")}
};
_apiFactory = new TestApiWebApplicationFactory<SampleApi.Startup>(_factory.CreateClient())
{
ClientOptions = {BaseAddress = new Uri("http://localhost:5001/")}
};
The TestJwtBearerOptions just proxies requests to identityServerClient. The implementation you can find here:
https://gist.github.com/ru-sh/048e155d73263912297f1de1539a2687
If you don't want to rely on a static variable to hold the HttpHandler, I've found the following to work. I think it's a lot cleaner.
First create an object that you can instantiate before your TestHost is created. This is because you won't have the HttpHandler until after the TestHost is created, so you need to use a wrapper.
public class TestHttpMessageHandler : DelegatingHandler
{
private ILogger _logger;
public TestHttpMessageHandler(ILogger logger)
{
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
_logger.Information($"Sending HTTP message using TestHttpMessageHandler. Uri: '{request.RequestUri.ToString()}'");
if (WrappedMessageHandler == null) throw new Exception("You must set WrappedMessageHandler before TestHttpMessageHandler can be used.");
var method = typeof(HttpMessageHandler).GetMethod("SendAsync", BindingFlags.Instance | BindingFlags.NonPublic);
var result = method.Invoke(this.WrappedMessageHandler, new object[] { request, cancellationToken });
return await (Task<HttpResponseMessage>)result;
}
public HttpMessageHandler WrappedMessageHandler { get; set; }
}
Then
var testMessageHandler = new TestHttpMessageHandler(logger);
var webHostBuilder = new WebHostBuilder()
...
services.PostConfigureAll<JwtBearerOptions>(options =>
{
options.Audience = "http://localhost";
options.Authority = "http://localhost";
options.BackchannelHttpHandler = testMessageHandler;
});
...
var server = new TestServer(webHostBuilder);
var innerHttpMessageHandler = server.CreateHandler();
testMessageHandler.WrappedMessageHandler = innerHttpMessageHandler;