SignalR authentication with Bearer Token Step By Step


I have a project that involves using SignalR from a JavaScript application. Since the client is coming from a browser, my application employs JWT authentication for the SignalR client. 

In my WebAPI, I have JWT Bearer authentication configured. However, when attempting to use SignalR hub with authentication, I encountered errors, and essentially, I wasn't sure how to call the secure SignalR endpoint. After extensive research on the internet, I have found a solution that I'm going to share in this post so let's discuess about How to call signalr secure endpoint using jwt token core c# server.

In this post we are going to discuess the below point

  • How to make SignalR Bearer Authentication Secure
  • How to use JwtBearer Token to authenticate SignalR client
  • SignalR - authenticating with access token in .net core

Here is My Hub
We have added [Authorize] attribute to restricts access to the ChatHub to authenticated users only
 
[Authorize]
    public class ChatHub : Hub
    {
        private readonly IServiceScopeFactory _scopeFactory;

        public ChatHub(IServiceScopeFactory scopeFactory)
        {
            _scopeFactory = scopeFactory;
        }
        
        public override Task OnDisconnectedAsync(Exception exception)
        {
            using (var scope = _scopeFactory.CreateScope())
            {

                var _dbContext = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
             
                var userId =  new Guid(Context.User.GetLoggedInUserClaim(ClaimType.UserId));
                var user = _dbContext.Users.FirstOrDefault(a => a.Id == userId);
                user.IsOnline = false;
                user.LastDisconnectedAt = DateTime.UtcNow;
                _dbContext.SaveChanges();
                Debug.WriteLine("Client disconnected: " + Context.ConnectionId);
                return base.OnDisconnectedAsync(exception);
            }
           
        }
        public override Task OnConnectedAsync()
        {
            using (var scope = _scopeFactory.CreateScope())
            {
                Debug.WriteLine("Client connected: " + Context.ConnectionId);
                var _dbContext = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
                
                var userId = new Guid(Context.User.GetLoggedInUserClaim(ClaimType.UserId));
                var user = _dbContext.Users.FirstOrDefault(a => a.Id == userId);
                user.ConnectionId = Context.ConnectionId;
                user.IsOnline = true;
                user.LastConnectedAt = DateTime.UtcNow;
                _dbContext.SaveChanges();
                return base.OnConnectedAsync();
            }

        }
        public async Task SendNotification(string userId)
        {
            if (Clients != null)
            {
                await Clients.Group(userId.ToLower()).SendAsync("Notification", "New notification recived!");
            }

        }        
       
    }
Here is my Program.cs file
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
builder.Services.AddDbContext<DatabaseContext>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "PetsDevoteeBack", Version = "v1" });

    var securitySchema = new OpenApiSecurityScheme
    {
        Description = @"JWT Authorization header using the Bearer scheme. <br>
                                    Enter 'Bearer' [space] and then your token in the text input below. <br>
                                    Example: 'Bearer 12345abcdef'",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
        Scheme = "bearer",
        Reference = new OpenApiReference
        {
            Type = ReferenceType.SecurityScheme,
            Id = "Bearer"
        }
    };

    c.AddSecurityDefinition("Bearer", securitySchema);

    var securityRequirement = new OpenApiSecurityRequirement
                {
                    { securitySchema, new[] { "Bearer" } }
                };

    c.AddSecurityRequirement(securityRequirement);
});
builder.Services.AddSignalR();
builder.Services.AddSingleton<ChatHub>();
builder.Services.AddSingleton<JWTManagerRepository>();
builder.Services.AddSingleton<DbThreadLogic>();

// JWT
builder.Services.AddAuthentication(x =>
{
    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
    var Key = Encoding.UTF8.GetBytes(builder.Configuration["JWT:Key"]);
    o.SaveToken = true;
    o.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = false,
        ValidateAudience = false,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = builder.Configuration["JWT:Issuer"],
        ValidAudience = builder.Configuration["JWT:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(Key)
    };
});
builder.Services.AddAuthorization(x =>
{
    x.AddPolicy(AccessLevels.GlobalKey, policy => policy.RequireClaim("Type", ((int)Permissions.Global).ToString()));
});
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
})
  .AddCookie()
  .AddGoogle(options =>
  {
      options.ClientId = builder.Configuration["Authentication:Google:ClientId"];
      options.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"];
  });
builder.Services.AddCors(p => p.AddDefaultPolicy(builder =>
{
    builder.WithOrigins("*").AllowAnyMethod().AllowAnyHeader();
}));
var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
else
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseCors(builder => builder
                .AllowAnyHeader()
                .AllowAnyMethod()
                .SetIsOriginAllowed((host) => true)
                .AllowCredentials()
              );
app.UseAuthentication();
app.UseAuthorization();

app.UseStaticFiles();
app.MapControllers();
app.MapHub<ChatHub>("/Chating");
app.Run();

To secure a SignalR endpoint with JWT bearer tokens, we have configure our SignalR application to authenticate users using JWT bearer tokens. When a user logs in our application, we generate a JWT token for them. 
On each SignalR connection attempt, we are validate the JWT token sent by the client. Ensure the token is valid, hasn't expired, and the signature is correct. Authorization logic to determine whether the user associated with the token has permission to connect to the SignalR hub. we are using the claims within the JWT token to make authorization decisions.

Call a SignalR endpoint with security from a JavaScript client,


Get a JWT token, use the JWT token to establish a connection to the SignalR hub from JavaScript client,pass the JWT token as a bearer token in the connection request header.

Example of how you can connect to a SignalR hub with security using JavaScript:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.js"></script>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.0.1/css/toastr.css" rel="stylesheet" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.0.1/js/toastr.js"></script>
    
    <script type="text/javascript">
        // Authenticate user and obtain JWT token
        const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJUeXBlIjoiMCIsIlVzZXJJZCI6IjYxYTRmZjYwLWY2ZWYtZWUxMS05Y2JiLWE0YmYwMTZjZTU2ZCIsIm5iZiI6MTcxMjA2MTg4NiwiZXhwIjoxNzEyMTQ4Mjg2LCJpYXQiOjE3MTIwNjE4ODZ9.SPYHa4BnQlF-GRAeq7pyOlEZkpuJ3eXwcxIzcJCK2ts";
        const connection = new signalR.HubConnectionBuilder()
            .withUrl("https://localhost:7140/Chating",{
                accessTokenFactory: () => token
            })
            .configureLogging(signalR.LogLevel.Information)
            .build();

        async function start() {
            try {
                await connection.start();              
                console.log("SignalR Connected.");
                
            } catch (err) {
                console.log(err);
                setTimeout(start, 5000);
            }
        };

        connection.onclose(async () => {
            await start();
        });
        // Listen for events
        connection.on("Notification", async (message) => {          
            alert(message)
        });
        
        // Handle token expiration and renew the token if necessary
        // Example logic to renew token
        function renewToken() {
            // Implement token renewal logic in case if token expire
        }

        // Renew token every 60 minutes
        setInterval(renewToken, 60 * 60 * 1000);
        // Start the connection.
        start();       
    </script>
 Above code exanple establishes a connection to the SignalR hub using the provided JWT token for authentication. It also listens for events emitted by the hub and handles token expiration by renewing the token periodically.