I have below project structure:
In Project A I have below implementation for Login:
public async Task LoginUser()
{
if (Input.Username == "" && Input.Email == "")
{
errorMessage = "Username or Email is required for sign in";
return;
}
if (Input.Password == "")
{
errorMessage = "Password is required for sign in";
return;
}
try
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
ApplicationUser? userObj;
if (emailUsername_Switch)
userObj = await UserManager.FindByEmailAsync(Input.Email);
else
userObj = await UserManager.FindByNameAsync(Input.Username);
if (userObj == null)
{
errorMessage = "Unable to find user in the system. Please Register or check username/email.";
return;
}
Guid userId = new Guid();
if (userObj != null)
{
userId = userObj.Id;
}
isLoading = true;
if (await SignInManager.CanSignInAsync(userObj))
{
var result = await SignInManager.CheckPasswordSignInAsync(userObj, Input.Password, true);
if (result == Microsoft.AspNetCore.Identity.SignInResult.Success)
{
Guid key = Guid.NewGuid();
BlazorCookieLoginMiddleware.Logins[key] = new LoginInfo { Email = userObj.Email, UserName = userObj.UserName, Password = Input.Password };
// Generate JWT Token
var token = TokenService.GenerateToken(Input.Username);
// Store token in localStorage (or use other storage methods as required)
// Save the token to localStorage
await LocalStorageService.SetItemAsync("jwt_token", token);
NavigationManager.NavigateTo($"/login?key={key}", true);
}
else if (result == Microsoft.AspNetCore.Identity.SignInResult.LockedOut)
{
errorMessage = "User account locked please contact administrator";
}
else if (result == Microsoft.AspNetCore.Identity.SignInResult.Failed)
{
// Handle failure
// Get the number of attempts left
var attemptsLeft = UserManager.Options.Lockout.MaxFailedAccessAttempts - await UserManager.GetAccessFailedCountAsync(userObj);
errorMessage = $"Invalid Login Attempt. Remaining Attempts : {attemptsLeft}";
}
}
else
{
errorMessage = "Your account is blocked";
}
}
catch (Exception ex)
{
errorMessage = "Error: Invalid login attempt. Please check again.";
}
isLoading = false;
}
Below is my TokenService:
public class TokenService
{
private readonly string _secretKey;
private readonly string _issuer;
private readonly string _audience;
public TokenService(string secretKey, string issuer, string audience)
{
_secretKey = secretKey;
_issuer = issuer;
_audience = audience;
}
// Method to generate JWT token
public string GenerateToken(string username)
{
var claims = new[]
{
new Claim(ClaimTypes.Name, username),
// Add other claims as needed
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secretKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _issuer,
audience: _audience,
claims: claims,
expires: DateTime.Now.AddHours(1),
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
// Method to validate token and extract user info
public ClaimsPrincipal ValidateToken(string token)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secretKey));
var handler = new JwtSecurityTokenHandler();
try
{
var principal = handler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidIssuer = _issuer,
ValidAudience = _audience,
IssuerSigningKey = key
}, out var validatedToken);
return principal;
}
catch
{
return null;
}
}
}
In Project B I have below implementation for SignalR:
[Authorize]
public class NotificationHub : Hub
{
private static readonly ConcurrentDictionary<string, List<string>> UserConnections = new();
[Authorize]
public override Task OnConnectedAsync()
{
var userEmail = Context.User?.FindFirst(ClaimTypes.Email)?.Value;
//var userName = Context.User?.Identity?.Name; // Assuming the username is stored in the Name claim
if (!string.IsNullOrEmpty(userEmail))
{
UserConnections.AddOrUpdate(
userEmail,
new List<string> { Context.ConnectionId }, // Add a new list with the current connection ID
(key, existingConnections) =>
{
if (!existingConnections.Contains(Context.ConnectionId))
{
existingConnections.Add(Context.ConnectionId); // Add the connection ID to the existing list
}
return existingConnections;
});
}
return base.OnConnectedAsync();
}
[Authorize]
public override Task OnDisconnectedAsync(Exception exception)
{
var userEmail = Context.User?.FindFirst(ClaimTypes.Email)?.Value;
var connectionID = Context.ConnectionId;
if (!string.IsNullOrEmpty(userEmail))
{
if (UserConnections.TryGetValue(userEmail, out var connections))
{
// Remove the specific connection ID
connections.Remove(Context.ConnectionId);
// If no more connections exist for this user, remove the user entry from the dictionary
if (connections.Count == 0)
{
UserConnections.TryRemove(userEmail, out _);
}
}
}
return base.OnDisconnectedAsync(exception);
}
[Authorize]
public async Task SubscribeToTouchSiteGroup(Guid siteId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, SignalR_Method.TouchSiteGroup + "_" + siteId);
}
[Authorize]
public async Task UnSubscribeFromTouchSiteGroup(Guid siteId)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, SignalR_Method.TouchSiteGroup + "_" + siteId);
}
}
JWT Local Storage Service:
public class LocalStorageService
{
private readonly IJSRuntime _jsRuntime;
public LocalStorageService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public async Task SetItemAsync(string key, string value)
{
await _jsRuntime.InvokeVoidAsync("localStorageHelper.setItem", key, value);
}
public async Task<string> GetItemAsync(string key)
{
return await _jsRuntime.InvokeAsync<string>("localStorageHelper.getItem", key);
}
public async Task RemoveItemAsync(string key)
{
await _jsRuntime.InvokeVoidAsync("localStorageHelper.removeItem", key);
}
public async Task ClearAsync()
{
await _jsRuntime.InvokeVoidAsync("localStorageHelper.clear");
}
}
JS script for JWT Local Storage:
//////////////////////////////////Code for JWT Local Storage///////////////////////////////////////////
window.localStorageHelper = {
setItem: function (key, value) {
localStorage.setItem(key, value);
},
getItem: function (key) {
return localStorage.getItem(key);
},
removeItem: function (key) {
localStorage.removeItem(key);
},
clear: function () {
localStorage.clear();
}
};
Then I have centralised service that will be used for sharing data and starting SignalR Connection: (string baseUrl, string hubPath) are passed as parameters from each app specifying to open hubconnection for receiving. Means Project C will open for Project D baseURL.
public class ProductNotificationHubService
{
private readonly LocalStorageService _localStorageService;
private HubConnection? _hubConnection;
/// <summary>
///
/// </summary>
/// <param name="cacheService"></param>
/// <param name="productService"></param>
/// <param name="lockManagerService"></param>
public ProductNotificationHubService(LocalStorageService localStorageService)
{
_localStorageService = localStorageService;
}
/// <summary>
///
/// </summary>
/// <param name="baseUrl"></param>
/// <param name="hubPath"></param>
/// <param name="subscribeToGroup"></param>
/// <param name="siteId"></param>
/// <param name="userName"></param>
/// <returns></returns>
public async Task<bool> InitializeHubAsync(string baseUrl, string hubPath, string subscribeToGroup, Guid? siteId, string userName)
{
UserName = userName;
// Retrieve token from localStorage
var token = await _localStorageService.GetItemAsync("jwt_token");
// Initialize the SignalR connection and pass the token
_hubConnection = new HubConnectionBuilder()
.WithUrl(new Uri(new Uri(baseUrl), hubPath), options =>
{
options.AccessTokenProvider = () => Task.FromResult(token);
})
.WithAutomaticReconnect()
.Build();
_hubConnection.On<Guid?, ProductModel, string>(SignalR_Method.TouchProductReceiveNotification, async (siteID, product, messageType) =>
{
if (_subscribedMessageTypes.Contains(messageType))
{
await HandleNotificationAsync(siteID, product, messageType);
}
});
try
{
await _hubConnection.StartAsync();
await _hubConnection.InvokeAsync(subscribeToGroup, siteId);
return true;
}
catch (Exception ex)
{
return false;
// Handle exception (e.g., log it)
}
}
}
Now in Project C and Project D I have below code when calling and starting signalR connection:
var baseUrl = "https://localhost:7140"; // Project D's URL in project C for listening
var hubPath = "/notificationHub"; // Path to your hub
await ProductNotificationHubService.InitializeHubAsync(baseUrl, hubPath, SignalR_Method.SubscribeToTouchSiteGroup, Site.Site_ID, _userInfo.UserName)
Below is the Program.cs file for both Project D and Project C:
builder.Services.AddScoped<LocalStorageService>();
// Register TokenService directly
builder.Services.AddSingleton<TokenService>(provider =>
new TokenService(
builder.Configuration.GetSection("TokenSettings").GetValue<string>("Key"),
builder.Configuration.GetSection("TokenSettings").GetValue<string>("Issuer"),
builder.Configuration.GetSection("TokenSettings").GetValue<string>("Audience")
));
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
options.RequireAuthenticatedSignIn = true;
})
// Cookie based authentication for login validation
.AddCookie(options =>
{
options.LoginPath = "/Account/Login/";
options.LogoutPath = "/Account/Logout/";
options.AccessDeniedPath = "/Account/AccessDenied";
options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
options.Cookie.HttpOnly = true;
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromSeconds(30);
})
// JWT Bearer Authentication for API or SignalR clients
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = builder.Configuration.GetSection("TokenSettings").GetValue<string>("Issuer"),
ValidateIssuer = true,
ValidAudience = builder.Configuration.GetSection("TokenSettings").GetValue<string>("Audience"),
ValidateAudience = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration.GetSection("TokenSettings").GetValue<string>("Key"))),
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
options.IncludeErrorDetails = true;
// Use Authorization header for SignalR
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
// Extract token from query string for SignalR
var accessToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(accessToken))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
}).AddIdentityCookies();
Now please note that my current authentication is working for both apps using cookies auth which I want to continue to use. Only want to use JWT authentication for SignalR to be able to get Context details (var userEmail = Context.User?.FindFirst(ClaimTypes.Email)?.Value;
).
Now in this the issue I am facing is still the same the JWT token gets added to local storage however, in Program.cs file I always get empty string for below:
// Extract token from query string for SignalR
var accessToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(accessToken))
{
context.Token = accessToken;
}
And context is still blank, I feel I am missing something please advise ?
Since you are using custom jwt token service, you need to add custom scheme for it. Please change your settings like below.
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
options.RequireAuthenticatedSignIn = true;
})
// add below settings for signalr
.AddJwtBearer("SignalRJwtScheme", options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration.GetSection("TokenSettings").GetValue<string>("Issuer"),
ValidAudience = builder.Configuration.GetSection("TokenSettings").GetValue<string>("Audience"),
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration.GetSection("TokenSettings").GetValue<string>("Key"))),
};
})
// Cookie based authentication for login validation
.AddCookie(options =>
{
options.LoginPath = "/Account/Login/";
options.LogoutPath = "/Account/Logout/";
options.AccessDeniedPath = "/Account/AccessDenied";
options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
options.Cookie.HttpOnly = true;
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromSeconds(30);
})
.AddIdentityCookies();
And use it in your signalr hub.
[Authorize(AuthenticationSchemes = "SignalRJwtScheme")]
public class NotificationHub : Hub
Test Result