DEV Community

Masui Masanori
Masui Masanori

Posted on

3 1

[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

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay