Intro
This time I will try adding some functions into my WebSocket application.
Environments
- .NET ver.6.0.202
- NLog.Web.AspNetCore ver.4.14.0
- Microsoft.EntityFrameworkCore ver.6.0.4
- Microsoft.EntityFrameworkCore.Design ver.6.0.4
- Npgsql.EntityFrameworkCore.PostgreSQL ver.6.0.3
- Microsoft.AspNetCore.Identity.EntityFrameworkCore ver.6.0.4
- Microsoft.AspNetCore.Authentication.JwtBearer ver.6.0.4
- Node.js ver.17.9.0
- TypeScript ver.4.6.3
- ws ver.7.4.0
- webpack ver.5.70.0
Authentication
WebSocket has no specifications regarding authentication.
I found many samples authenticates by cookie, session or adding tokens as URL parameters.
This time, I add JWT into session.
Program.cs
using System.Net;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using NLog.Web;
using WebRtcSample.Models;
using WebRtcSample.Users;
using WebRtcSample.Users.Repositories;
using WebRtcSample.WebSockets;
var logger = NLogBuilder.ConfigureNLog(Path.Combine(Directory.GetCurrentDirectory(), "Nlog.config"))
.GetCurrentClassLogger();
try
{
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("http://0.0.0.0:5027");
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
ClockSkew = TimeSpan.FromSeconds(30),
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
};
});
builder.Services.AddSession(options => {
options.IdleTimeout = TimeSpan.FromSeconds(30);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
options.Cookie.SameSite = SameSiteMode.Strict;
});
builder.Services.AddRazorPages();
builder.Services.AddControllers();
builder.Services.AddHttpContextAccessor();
builder.Services.AddDbContext<SampleContext>(options =>
{
options.EnableSensitiveDataLogging();
options.UseNpgsql(builder.Configuration["DbConnection"]);
});
builder.Services.AddIdentity<ApplicationUser, IdentityRole<int>>()
.AddUserStore<ApplicationUserStore>()
.AddEntityFrameworkStores<SampleContext>()
.AddDefaultTokenProviders();
builder.Services.AddSingleton<IWebSocketHolder, WebSocketHolder>();
builder.Services.AddScoped<IApplicationUserService, ApplicationUserService>();
builder.Services.AddScoped<IUserTokens, UserTokens>();
builder.Services.AddScoped<IApplicationUsers, ApplicationUsers>();
var app = builder.Build();
app.UseSession();
// this line must be executed before UseRouting().
app.Use(async (context, next) =>
{
var token = context.Session.GetString("user-token");
if(string.IsNullOrEmpty(token) == false)
{
context.Request.Headers.Add("Authorization", $"Bearer {token}");
}
await next();
});
app.UseStaticFiles();
app.UseWebSockets();
app.UseStatusCodePages(async context =>
{
if (context.HttpContext.Response.StatusCode == (int)HttpStatusCode.Unauthorized)
{
if(context.HttpContext.Request.Path.StartsWithSegments("/") ||
context.HttpContext.Request.Path.StartsWithSegments("/pages"))
{
context.HttpContext.Response.Redirect("/pages/signin");
return;
}
}
await context.Next(context.HttpContext);
});
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
// this line must be executed after setting tokens and authentications.
app.MapWebSocketHolder("/ws");
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.Run();
}
catch (Exception ex) {
logger.Error(ex, "Stopped program because of exception");
throw;
}
finally {
NLog.LogManager.Shutdown();
}
The session values will kept after connecting WebSocket.
WebSocketHolder.cs
using System.Collections.Concurrent;
using System.Net.WebSockets;
namespace WebRtcSample.WebSockets
{
public class WebSocketHolder: IWebSocketHolder
{
private readonly ILogger<WebSocketHolder> logger;
private readonly IHttpContextAccessor httpContext;
private readonly ConcurrentDictionary<string, WebSocket> clients = new ();
private CancellationTokenSource source = new ();
public WebSocketHolder(ILogger<WebSocketHolder> logger,
IHostApplicationLifetime applicationLifetime,
IHttpContextAccessor httpContext)
{
this.logger = logger;
applicationLifetime.ApplicationStopping.Register(OnShutdown);
this.httpContext = httpContext;
}
private void OnShutdown()
{
source.Cancel();
}
public async Task AddAsync(HttpContext context)
{
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
if(clients.TryAdd(CreateId(), webSocket))
{
await EchoAsync(webSocket);
}
}
private string CreateId()
{
return Guid.NewGuid().ToString();
}
private async Task EchoAsync(WebSocket webSocket)
{
try
{
// for sending data
byte[] buffer = new byte[1024 * 4];
while(true)
{
string? token = this.httpContext.HttpContext?.Session?.GetString("user-token");
string? userId = this.httpContext.HttpContext?.User?.Identity?.Name;
bool? authenticated = this.httpContext.HttpContext?.User?.Identity?.IsAuthenticated;
logger.LogDebug($"Echo Token: {token} User: {userId} auth?: {authenticated}");
WebSocketReceiveResult result = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), source.Token);
if(result.CloseStatus.HasValue)
{
await webSocket.CloseAsync(result.CloseStatus.Value,
result.CloseStatusDescription, source.Token);
clients.TryRemove(clients.First(w => w.Value == webSocket));
webSocket.Dispose();
break;
}
// Send to all clients
foreach(var c in clients)
{
if(c.Value == webSocket)
{
continue;
}
await c.Value.SendAsync(
new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType,
result.EndOfMessage, source.Token);
}
}
}
catch(OperationCanceledException ex)
{
logger.LogError($"Exception {ex.Message}");
}
}
}
}
One important thing is the session values, the cookie values, and signing in status will be kept until closing WebSocket connections.
Top comments (0)