DEV Community

Masui Masanori
Masui Masanori

Posted on

[ASP.NET Core]Try WebSocket

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();
}
Enter fullscreen mode Exit fullscreen mode

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}");
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

One important thing is the session values, the cookie values, and signing in status will be kept until closing WebSocket connections.

Resources

Top comments (0)