-
Notifications
You must be signed in to change notification settings - Fork 446
fix issue with incorrect user detection when Invoking for User #747
Conversation
This seems like a small (but important) contribution, so no Contribution License Agreement is required at this point. We will now review your pull request. |
@@ -104,7 +104,7 @@ public override Task InvokeGroupAsync(string groupName, string methodName, objec | |||
public override Task InvokeUserAsync(string userId, string methodName, object[] args) | |||
{ | |||
return InvokeAllWhere(methodName, args, connection => | |||
string.Equals(connection.User.FindFirst(ClaimTypes.NameIdentifier).Value, userId, StringComparison.Ordinal)); | |||
string.Equals(connection.User.FindFirstValue(ClaimTypes.NameIdentifier), userId, StringComparison.Ordinal)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about connection.User.FindFirst(ClaimTypes.NameIdentifier)?.Value
and not taking the dependency on Microsoft.Extensions.Identity.Core?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-- removed -- Sorry, missed .?
operator in your peace of code. Ok, I will do that.
Connection.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, Interlocked.Increment(ref _id).ToString()) })); | ||
|
||
string claimValue = Interlocked.Increment(ref _id).ToString(); | ||
List<Claim> claims = new List<Claim>{ new Claim(ClaimTypes.Name, claimValue) }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var
@@ -38,7 +38,15 @@ public TestClient(bool synchronousCallbacks = false) | |||
_transport = ChannelConnection.Create<byte[]>(input: transportToApplication, output: applicationToTransport); | |||
|
|||
Connection = new DefaultConnectionContext(Guid.NewGuid().ToString(), _transport, Application); | |||
Connection.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, Interlocked.Increment(ref _id).ToString()) })); | |||
|
|||
string claimValue = Interlocked.Increment(ref _id).ToString(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var
@@ -28,7 +28,7 @@ public class TestClient : IDisposable, IInvocationBinder | |||
public Channel<byte[]> Application { get; } | |||
public Task Connected => ((TaskCompletionSource<bool>)Connection.Metadata["ConnectedTask"]).Task; | |||
|
|||
public TestClient(bool synchronousCallbacks = false) | |||
public TestClient(bool synchronousCallbacks = false, bool shouldHaveId = false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider renaming toshouldHaveId
to addClaimId
- as it is now it is not clear what the id refers to
@@ -53,6 +61,7 @@ public TestClient(bool synchronousCallbacks = false) | |||
} | |||
} | |||
|
|||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: remove
/cc @BrennanConroy - can you also take a look? |
Redis will have a similar issue I believe
|
We need to introduce that service so we can share the logic. |
@@ -52,7 +60,7 @@ public TestClient(bool synchronousCallbacks = false) | |||
Application.Out.TryWrite(memoryStream.ToArray()); | |||
} | |||
} | |||
|
|||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: remove
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my editor there is single line freespace as in other places across the file. Not sure why it shows line added there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a line containing whitespaces - if you are using VS you can install this extension and you will never miss it.
@@ -28,7 +28,7 @@ public class TestClient : IDisposable, IInvocationBinder | |||
public Channel<byte[]> Application { get; } | |||
public Task Connected => ((TaskCompletionSource<bool>)Connection.Metadata["ConnectedTask"]).Task; | |||
|
|||
public TestClient(bool synchronousCallbacks = false) | |||
public TestClient(bool synchronousCallbacks = false, bool addClaimId = false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was just thinking - is there any reason to not add the claimId always (i.e. can we even not pass this parameter)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having this param permanent will require to fix test case with unauthorized user, since it's currently check that user have no NameIdentifier
claim (well, testcase not throwing unauthorized exception if user have those claim, but case expect it will be thrown).
So avoid that kind of parameter will probably require some test code changes and introducing 2 types of clients - authorized and unauthorized. Wasn't sure if that's something to make sense to do, so decide just start with additional parameter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leave as it is then for now.
@@ -187,7 +188,7 @@ public override Task OnConnectedAsync(HubConnectionContext connection) | |||
|
|||
if (connection.User.Identity.IsAuthenticated) | |||
{ | |||
var userChannel = _channelNamePrefix + ".user." + connection.User.Identity.Name; | |||
var userChannel = _channelNamePrefix + ".user." + connection.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there isn't a value then we probably shouldn't be subscribing to _channelNamePrefix + ".user."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, you need to have this below where we publish to the user channel
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it a valid case that user is Authenticated but don't have NameIdentifier
claim?
Also, you need to have this below where we publish to the user channel
Sorry, not sure I get that correctly. You mean InvokeUserAsync
method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@BrennanConroy Ok, let's assume it's possible case, that user authenticated, but still have no NameIdentifier
claim. What behavior you expect to see there. Just do nothing or throwing InvalidOperationException
?
@davidfowl made me aware about this issue: #604. Looks like this is the way to go here... |
@moozzyk Should I implement DefaultUserIdProvider, add it to signalr configuration dependency injections and than change this part of code to use this provider? |
Yep! |
@ivankarpey Any progress on this change? |
Sorry Guys. Was on vac. I will provide updated version before Monday. |
return string.Equals(connection.User.Identity.Name, userId, StringComparison.Ordinal); | ||
}); | ||
return InvokeAllWhere(methodName, args, connection => | ||
string.Equals(_userIdProvider.GetUserId(connection), userId, StringComparison.Ordinal)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets run this in OnConnectedAsync and cache it on the connection object itself.
{ | ||
public interface IUserIdProvider | ||
{ | ||
string GetUserId(HubConnectionContext conn); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: connection
@@ -245,9 +249,9 @@ public override Task OnConnectedAsync(HubConnectionContext connection) | |||
previousConnectionTask = WriteAsync(connection, message); | |||
}); | |||
|
|||
if (connection.User.Identity.IsAuthenticated) | |||
if (connection.User.Identity.IsAuthenticated && _userIdProvider.GetUserId(connection) != null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't check for authenticated, just check for null and store the result in a local so it's not executed twice.
@@ -25,12 +26,15 @@ public class HubConnectionContext | |||
private readonly ConnectionContext _connectionContext; | |||
private readonly CancellationTokenSource _connectionAbortedTokenSource = new CancellationTokenSource(); | |||
private readonly TaskCompletionSource<object> _abortCompletedTcs = new TaskCompletionSource<object>(); | |||
private readonly IUserIdProvider _userIdProvider; | |||
private string _userIdCache = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Super nit: Just call this _userId
.
@@ -67,6 +71,18 @@ public virtual void Abort() | |||
Task.Factory.StartNew(_abortedCallback, this); | |||
} | |||
|
|||
public string UserIdentifier |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not comfortable with the lazy evaluation of this. I think it should be set by the HubEndPoint after negotiate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I don't really like the idea of having public setter for that sort of things. Since I think that for end-user connection context should be something "immutable". Don't you think so?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with that too, make it an internal setter. We'll need to see what mocking this object looks like though.
@@ -245,9 +245,9 @@ public override Task OnConnectedAsync(HubConnectionContext connection) | |||
previousConnectionTask = WriteAsync(connection, message); | |||
}); | |||
|
|||
if (connection.User.Identity.IsAuthenticated) | |||
if (!String.IsNullOrEmpty(connection.UserIdentifier)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: lowercase s in string
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
@@ -14,7 +14,7 @@ public class DefaultHubLifetimeManager<THub> : HubLifetimeManager<THub> | |||
{ | |||
private long _nextInvocationId = 0; | |||
private readonly HubConnectionList _connections = new HubConnectionList(); | |||
|
|||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: remove empty space
@@ -166,7 +163,7 @@ public override Task OnDisconnectedAsync(HubConnectionContext connection) | |||
if (connection.Output.TryWrite(hubMessage)) | |||
{ | |||
break; | |||
} | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: remove space
At the current moment in
DefaultHubLifetimeManager<THub>.InvokeUserAsync
user detected byName
, not byId
, even ifuserId
is required via method argument list. That's incorrect behavior and could lead to data leaks to other user, since it's not guaranteed that user name is unique.Id
should be used there as it required via method arguments.