[ASP.NET Core]Try WebSocket
Tags: charp, aspnetcore
Published: 2022-04-19 17:25:27

## Intro
This time I will try adding some functions into my WebSocket application.

* [【ASP.NET Core】【TypeScript】Send messages with WebSockets](https://dev.to/masanori_msl/asp-net-core-typescript-send-messages-with-websockets-3hnl)

### 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.

* [Authenticating Websockets - Klee Thomas - Dev Community](https://dev.to/kleeut/authenticating-websockets-21lo)

This time, I add JWT into session.

* [[ASP.NET Core][Entity Framework Core] Try JWT 1](https://dev.to/masanori_msl/aspnet-coreentity-framework-core-try-jwt-1-357a)
* [[ASP.NET Core][Entity Framework Core] Try JWT 2](https://dev.to/masanori_msl/aspnet-coreentity-framework-core-try-jwt-2-2iij)

#### Program.cs
```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
```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.

## Resources
* [RFC6455: The WebSocket Protocol](https://www.rfc-editor.org/rfc/rfc6455.html)
* [WebSocket - Web APIs|MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)