(Blazor-server-side / SignalR / Net6) HubConnectionContext loses user in HttpContext the second time it's called - signalr

I am trying to send a message with SignalR to a specific user.
I implemented the default project authentication with Blazor Server side and Net6.
I can log in / log out / register.
I implemented the IUSerIdProvider Interface to get the UserId.
The first time I launch the app, I can retrieved the user (from connection.GetHttpContext(); or connection.User.FindFirstValue(ClaimTypes.Name); but when I navigate to an other page and call the hub again, the HubConnectionContext loses my User and all his informations.
If I force the id with a constant string it works but why do I lose the informations the second time ?
I don't know if I need to use cookies because the first time I have informations.
// CustomUserIdProvider.cs
public class CustomUserIdProvider : IUserIdProvider
{
public string? GetUserId(HubConnectionContext connection)
{
var httpContext = connection.GetHttpContext();
var userId = connection.User.FindFirstValue(ClaimTypes.Name);
if (string.IsNullOrWhiteSpace(userId))
return string.Empty;
return userId;
}
}
// Program.cs
-----
builder.Services.AddSingleton<IUserIdProvider, CustomUserIdProvider>();
-----
app.UseAuthentication();
app.UseAuthorization();
// SignalR.razor (where I test to receive / send a message and here I lost the informations)
protected override async Task OnInitializedAsync()
{
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/notifyhub"))
.Build();
hubConnection.On<int, string>("ReceiveMessage", (id, message) =>
{
var encodedMsg = $"{id}: {message}";
InvokeAsync(StateHasChanged);
});
await hubConnection.StartAsync();
}
private async Task Send()
{
if (hubConnection is not null)
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
authMessage = $"{user.Identity.Name} is authenticated.";
claims = user.Claims;
surnameMessage =
$"Surname: {user.FindFirst(c => c.Type == ClaimTypes.Surname)?.Value}";
await hubConnection.SendAsync("Send", user.Identity.Name, 1, "Message envoyé");
}
}

Related

How to unsubscribe email from AWS SNS Topic?

I have an endpoint that subscribes the specified email to my SNS topic:
[HttpPost("subscriptions/{email}")]
public async Task SubscribeEmail(string email)
{
try
{
var request = new SubscribeRequest()
{
TopicArn = AwsServicesConstants.SenderTopicArn,
ReturnSubscriptionArn = true,
Protocol = "email",
Endpoint = email,
};
var response = await _snsClient.SubscribeAsync(request);
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected error: {ex}");
}
}
How can I unsubscribe given email from that topic with just a specified email like this
[HttpDelete("subscriptions/{email}")]
public async Task<UnsubscribeResponse> UnsubscribeEmail(string email)
{
var request = new UnsubscribeRequest(email);
var response = await _snsClient.UnsubscribeAsync(request);
return response;
}
Actually, unsubscription is not working because UnsubscribeRequest requires only subscriptionArn, and not the email
You will need to identify the subscription (once subscribed) by calling ListSubscriptionsByTopic, looking for the Endpoint that matches the desired email address. You could then extract the ARN and use it when calling Unsubscribe.
You can write app logic to get the ARN value using the email address. Here is a C# example that shows you the logic for this use case using the AWS SDK for .NET.
public async Task<string> UnSubEmail(string email)
{
var client = new AmazonSimpleNotificationServiceClient(RegionEndpoint.USEast2);
var arnValue = await GetSubArn(client, email);
await RemoveSub(client, arnValue);
return $"{email} was successfully deleted!";
}
public static async Task<string> GetSubArn(IAmazonSimpleNotificationService client, string email)
{
var request = new ListSubscriptionsByTopicRequest();
request.TopicArn = TopicArn;
var subArn = string.Empty;
var response = await client.ListSubscriptionsByTopicAsync(request);
List<Subscription> allSubs = response.Subscriptions;
// Get the ARN Value for this subscription.
foreach (Subscription sub in allSubs)
{
if (sub.Endpoint.Equals(email))
{
subArn = sub.SubscriptionArn;
return subArn;
}
}
return string.Empty;
}
public static async Task<string> RemoveSub(IAmazonSimpleNotificationService client, string subArn)
{
var request = new UnsubscribeRequest();
request.SubscriptionArn = subArn;
await client.UnsubscribeAsync(request);
return string.Empty;
}
You can find full .NET Example in the AWS Code Lib:
Build a publish and subscription application that translates messages

Hot to get count of connection in SignalR Core Group

How can I get count of connections in specific SignalR Core group?
Something like that:
this.Clients.Group("Something").Count();
It would be also good if SendAsync would return how many clients send message:
var count = await this.Clients.Client(this.Context.ConnectionId).SendAsync("msg", msg);
I need this to determine if group is empty. Data should be correct if user disconnects from SignalR Core with force (unplug net cable)...
You can use the OnConnected and OnDisconnected Events and save the connection / connectionIds.
I use something like this with additional informations:
internal static ConcurrentDictionary<string, ConnectionInfos> Users = new ConcurrentDictionary<string, ConnectionInfos>();
public override async Task OnConnectedAsync()
{
var connId = Context.ConnectionId;
if (!Users.Keys.Any(x => x == connId))
{
Users.TryAdd(connId, new ConnectionInfos { /*...*/});
}
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
var id = Context.ConnectionId;
if (Users.TryRemove(id, out ConnectionInfos ci))
{
//Connection removed
}
await base.OnDisconnectedAsync(exception);
}

How to invalidate 'expired' Firebase Instance ID Token

AFAIK, the Firebase Instance Token will be refreshed under the following 4 conditions:
App deletes Instance ID
App is restored on a new device
User uninstalls/reinstall the app
User clears app data
Suppose a user is using Token A as his 'FCM address'. Every time when he logs in the app, he will register the Token A to the Firestore along with this user's UUID so user-specific cloud message can be sent to him. When he logs out, the system will fire a request to firestore for removing the token A record.
Now, when the user reinstalls the app, the instance id is refreshed and a new Token B is generated. The Token A becomes useless. Unfortunately, if the user does not log out before the uninstallation, token A will stay in the firestore forever.
Any workaround or wiser way to handle this case?
Keeping your token registry up to date requires two steps:
Remove outdated tokens from your application code.
Check for outdated tokens and remove them when you send messages.
Your approach of removing a token that is no longer used, is #1.
The second step though is to remove tokens from your registry/database when you get a messaging/invalid-registration-token or messaging/registration-token-not-registered response when trying to send a message to it. The functions-samples repo contains a great example of this:
admin.messaging().sendToDevice(tokens, payload).then((response) => {
// For each message check if there was an error.
const tokensToRemove = [];
response.results.forEach((result, index) => {
const error = result.error;
if (error) {
console.error('Failure sending notification to', tokens[index], error);
// Cleanup the tokens who are not registered anymore.
if (error.code === 'messaging/invalid-registration-token' ||
error.code === 'messaging/registration-token-not-registered') {
// TODO: remove the token from your registry/database
}
}
});
});
The above code uses the Firebase Admin SDK for Node.js, but the same logic could also be applied to other platforms or when sending messages through the HTTPS endpoints.
As Frank mentioned in his answer you can remove them when sending messages and getting not registered error.
Here how I delete outdated registration tokens when registering a new one using C#.
First of all using Instance ID API I get token info as following:
public async Task<FCMTokenInfo> GetTokenInfoAsync(string token)
{
try
{
HttpClient client = new HttpClient();
client.BaseAddress = new Uri("https://iid.googleapis.com");
client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", String.Format("key={0}", "your-authorization-key"));
var uri = $"/iid/info/{token}";
var httpResponse = await client.GetAsync(uri);
var responseStr = await httpResponse.Content.ReadAsStringAsync();
if (httpResponse.StatusCode != HttpStatusCode.OK)
{
//log 400 bad request and do whatever you want
}
var result = JsonConvert.DeserializeObject<FCMTokenInfo>(responseStr);
return result;
}
catch (Exception ex)
{
//log the exception
throw;
}
}
FCMTokenInfo.cs
public class FCMTokenInfo
{
public string Application { get; set; }
public string Subtype { get; set; }
public string Scope { get; set; }
public string AuthorizedEntity { get; set; }
public string Platform { get; set; }
}
And then inside the service that saves registration tokens inside the database:
//this method gets called when a new token is sent by the javascript web app
public async Task AddTokenAsync(Guid accountId, string token)
{
try
{
//getting TokenInfo of the current token(or refreshed one for that app)
var fcmTokenInfo = await firebaseServices.GetTokenInfoAsync(token);
//adding the current token
dbContext.FcmRegisterationTokens.Add(new FcmRegisterationToken
{
Token = token,
AccountId = accountId,
AddingDate = DateTimeOffset.UtcNow,
Application = fcmTokenInfo.Application,
Subtype = fcmTokenInfo.Subtype,
AuthorizedEntity = fcmTokenInfo.AuthorizedEntity,
Scope = fcmTokenInfo.Scope,
Platform = fcmTokenInfo.Platform
});
var outdatedTokens = await dbContext.FcmRegisterationTokens
.Where(x => x.AccountId == accountId
&& x.Application == fcmTokenInfo.Application
&& x.Platform == fcmTokenInfo.Platform
).ToListAsync();
//remove them
dbContext.FcmRegisterationTokens.RemoveRange(outdatedTokens);
dbContext.SaveChanges();
}
catch (Exception)
{
throw;
}
}

Sending a message to a specific user via signalr

I am trying to use the new User Id provider specified in signalr 2 to send messages to a specific user. When I call the Clients.All method, I see this working as my javascript code gets called from the server and the ui produces some expected text for my test case. However, when I switch to Clients.User the client side code is never called from the server. I followed the code outlined in this example: SignalR - Sending a message to a specific user using (IUserIdProvider) *NEW 2.0.0*.
NotificationHub.cs:
public class NotificationHub : Hub
{
[Authorize]
public void NotifyUser(string userId, int message)
{
Clients.User(userId).DispatchMessage(message);
}
public override Task OnConnected()
{
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
return base.OnDisconnected(stopCalled);
}
public override Task OnReconnected()
{
return base.OnReconnected();
}
}
IUserIdProvider.cs:
public class UserIdProvider : IUserIdProvider
{
MemberService _memberService;
public UserIdProvider()
{
}
public string GetUserId(IRequest request)
{
long UserId = 0;
if (request.User != null && request.User.Identity != null &&
request.User.Identity.Name != null)
{
var currenUser = Task.Run(() => _memberService.FindByUserName(request.User.Identity.Name)).Result;
UserId = currenUser.UserId;
}
return UserId.ToString();
}
}
Startup.cs
HttpConfiguration config = GlobalConfiguration.Configuration;
config.Routes.MapHttpRoute(
"Default2",
"api/{controller}/{action}/{id}",
new { id = RouteParameter.Optional });
config.Routes.MapHttpRoute(
"DefaultApi2",
"api/{controller}/{id}",
new { id = RouteParameter.Optional });
app.Map("/signalr", map =>
{
map.UseCors(CorsOptions.AllowAll);
var idProvider = new UserIdProvider();
GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => idProvider);
map.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()
{
Provider = new QueryStringOAuthBearerAuthenticationProvider()
});
var hubConfiguration = new HubConfiguration
{
};
map.RunSignalR(hubConfiguration);
});
app.MapSignalR();
QuerstringOAuthBearerAuthenticationProvider:
public class QueryStringOAuthBearerAuthenticationProvider
: OAuthBearerAuthenticationProvider
{
public override Task RequestToken(OAuthRequestTokenContext context)
{
if (context == null) throw new ArgumentNullException("context");
// try to find bearer token in a cookie
// (by default OAuthBearerAuthenticationHandler
// only checks Authorization header)
var tokenCookie = context.OwinContext.Request.Cookies["BearerToken"];
if (!string.IsNullOrEmpty(tokenCookie))
context.Token = tokenCookie;
return Task.FromResult<object>(null);
}
}
Do I need to map the user to the connections myself using the IUserIdProvider through the OnConnected, OnDisconnected, etc. or does this happen automatically behind the scenes? Is there someone wrong in my posted code that could be a problem as well? I am running signalr from the same environment as my web api rest services, don't know if this makes a difference and using the default bearer token setup web api is using.
It would be far easier for you to create a group based on the connectionid of the connecting client, in the onConnected event and broadcast to the group that matches the connected id, that way if the client disconnects, when they reconnect they would simply belong to a new group the themselves. Unless of course you are required to have an authenticated user.

usermanager.addtorole - An asynchronous module or handler completed while an asynchronous operation was still pending

I am adding a user to my aspnetusers database and that is working fine. Then I am also trying to link them to an existing role. That is when I get the error: "An asynchronous module or handler completed while an asynchronous operation was still pending."
Here is my method with the problem code:
private async void checkOldDB(string email, string password)
{
bool isValidUser = false;
ReportsMvc.App_Code.BLL.FUN.cFunUser user = ReportsMvc.App_Code.DAL.FUN.cFunUserDB.getUser(email);
if (user != null)
{
isValidUser = PasswordHash.PasswordHash.ValidatePassword(password, user.Password);
if (!isValidUser)
{
isValidUser = PasswordHash.PasswordHash.ValidateHashes(password, user.Password);
}
}
if (isValidUser)
{
var user2 = new ApplicationUser { UserName = email, Email = email };
var result = await UserManager.CreateAsync(user2, password);
if (result.Succeeded)
{
string role = user.Role;
if (string.IsNullOrEmpty(role))
{
role = "User";
}
UserManager.AddToRole(user2.Id, role);
await SignInManager.SignInAsync(user2, isPersistent: false, rememberBrowser: false);
}
}
}
The line starting with "await SignInManager" was working fine. Then when I added in that code to AddToRole, I started getting the above error. This identity/authentication stuff is all very new to me.
You should change async void to async Task and await it where you call it.
As a general rule, you should avoid async void; it should only be used for event handlers. I describe this more in an MSDN article.

Resources