Skip to content
This repository has been archived by the owner on Dec 18, 2018. It is now read-only.

fix issue with incorrect user detection when Invoking for User #747

Merged
merged 14 commits into from
Oct 6, 2017

Conversation

ivankarpey
Copy link
Contributor

@ivankarpey ivankarpey commented Aug 22, 2017

At the current moment in DefaultHubLifetimeManager<THub>.InvokeUserAsync user detected by Name, not by Id, even if userId 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.

@dnfclas
Copy link

dnfclas commented Aug 22, 2017

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.
Thanks,
.NET Foundation Pull Request Bot

@@ -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));
Copy link
Contributor

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?

Copy link
Contributor Author

@ivankarpey ivankarpey Aug 22, 2017

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) };
Copy link
Contributor

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();
Copy link
Contributor

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)
Copy link
Contributor

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)
}
}


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove

@moozzyk
Copy link
Contributor

moozzyk commented Aug 22, 2017

/cc @BrennanConroy - can you also take a look?

@BrennanConroy
Copy link
Member

BrennanConroy commented Aug 22, 2017

Redis will have a similar issue I believe

var userChannel = typeof(THub).FullName + ".user." + connection.User.Identity.Name;

@davidfowl
Copy link
Member

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());
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove

Copy link
Contributor Author

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.

Copy link
Contributor

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)
Copy link
Contributor

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)?

Copy link
Contributor Author

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.

Copy link
Contributor

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;
Copy link
Member

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."

Copy link
Member

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

Copy link
Contributor Author

@ivankarpey ivankarpey Aug 23, 2017

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?

Copy link
Contributor Author

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 ?

@moozzyk
Copy link
Contributor

moozzyk commented Aug 25, 2017

@davidfowl made me aware about this issue: #604. Looks like this is the way to go here...

@ivankarpey
Copy link
Contributor Author

@moozzyk Should I implement DefaultUserIdProvider, add it to signalr configuration dependency injections and than change this part of code to use this provider?

@davidfowl
Copy link
Member

@moozzyk Should I implement DefaultUserIdProvider, add it to signalr configuration dependency injections and than change this part of code to use this provider?

Yep!

@BrennanConroy
Copy link
Member

@ivankarpey Any progress on this change?

@ivankarpey
Copy link
Contributor Author

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));
Copy link
Member

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);
Copy link
Member

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)
Copy link
Member

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;
Copy link
Member

@davidfowl davidfowl Oct 6, 2017

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
Copy link
Member

@davidfowl davidfowl Oct 6, 2017

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.

Copy link
Contributor Author

@ivankarpey ivankarpey Oct 6, 2017

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?

Copy link
Member

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))
Copy link
Member

@davidfowl davidfowl Oct 6, 2017

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

Copy link
Member

@davidfowl davidfowl left a 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();

Copy link
Member

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;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove space

@davidfowl davidfowl dismissed moozzyk’s stale review October 6, 2017 18:55

Changes were made

@davidfowl davidfowl merged commit 665f166 into aspnet:dev Oct 6, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants