The video-call picker lists the current user, and clicking yourself starts a call that rings your own browser. This is the residual root cause behind #98 — that ticket fixed the reactivity race (read userIdSig instead of the mutated userId string), but the filter it repaired compares two different Keycloak claims, so it never matches regardless of timing.
Root cause — two ID spaces
ChatHub.OnConnectedAsync stores the roster entry with Id = sid (Keycloak session id) — SpikerSoft.Contracts.SignalR/ChatHub.cs:205. The whole of ChatHub is sid-keyed (DM participants, rooms, history).
Angular sets chatService.userId / userIdSig from keycloak.loadUserProfile().id, i.e. the sub user id — _services/chat/chat.service.ts:98-99 and :438-439.
So u.id (sid) !== selfId (sub) is always true and the filter is a no-op:
The backend self-guard is defeated by the same skew:
// VideoCallHub.cs:187-188if(calleeUserId==callerId)// calleeUserId is a sid, callerId is GetCurrentUserId() = subthrownewHubException("Cannot call yourself");
And because #99's fix subscribes each connection to a user-group for every claim variant (GetAllUserIdentifiers, VideoCallHub.cs:394-408), Clients.Group(UserGroup(calleeUserId)) delivers the invite straight back to the caller's own browser. The two fixes compose into a working self-call.
Why the tests didn't catch it
video-call-picker.component.spec.ts:56-61 sets userIdSig = signal("me") alongside a user("me") — ids constructed to match, so it passes whatever the real claim skew is.
Fix
Backend — VideoCallHub.InviteUser guards against GetAllUserIdentifiers() rather than GetCurrentUserId() alone, so a sid-addressed self-invite is rejected. Immune to ID-space drift (compares within one connection's own claims).
Frontend — align chatService.userId / userIdSig to the sid claim so it matches the roster identity ChatHub actually broadcasts. Every consumer of chatService.userId is hub-facing (chat.component.ts:177,182,238, chat.service.ts:371), so this also fixes DM "other participant" resolution, which is broken for the same reason.
Both are needed: the backend guard alone leaves you listed (just uninvitable); the frontend alone leaves the server unenforced.
#99 — alias user-groups, closed; interacts to make self-call actually connect
## Summary
The video-call picker lists the current user, and clicking yourself starts a call that **rings your own browser**. This is the residual root cause behind #98 — that ticket fixed the *reactivity race* (read `userIdSig` instead of the mutated `userId` string), but the filter it repaired compares two **different Keycloak claims**, so it never matches regardless of timing.
## Root cause — two ID spaces
- `ChatHub.OnConnectedAsync` stores the roster entry with `Id = sid` (Keycloak **session** id) — `SpikerSoft.Contracts.SignalR/ChatHub.cs:205`. The whole of `ChatHub` is `sid`-keyed (DM participants, rooms, history).
- Angular sets `chatService.userId` / `userIdSig` from `keycloak.loadUserProfile().id`, i.e. the **`sub`** user id — `_services/chat/chat.service.ts:98-99` and `:438-439`.
So `u.id (sid) !== selfId (sub)` is **always true** and the filter is a no-op:
- `_dialogs/video-call-picker/video-call-picker.component.ts:324-327`
- `_components/chat/chat.component.ts:64-72` (same broken filter, DM list)
The backend self-guard is defeated by the same skew:
```csharp
// VideoCallHub.cs:187-188
if (calleeUserId == callerId) // calleeUserId is a sid, callerId is GetCurrentUserId() = sub
throw new HubException("Cannot call yourself");
```
And because #99's fix subscribes each connection to a user-group for **every** claim variant (`GetAllUserIdentifiers`, `VideoCallHub.cs:394-408`), `Clients.Group(UserGroup(calleeUserId))` delivers the invite straight back to the caller's own browser. The two fixes compose into a working self-call.
## Why the tests didn't catch it
`video-call-picker.component.spec.ts:56-61` sets `userIdSig = signal("me")` alongside a `user("me")` — ids constructed to match, so it passes whatever the real claim skew is.
## Fix
1. **Backend** — `VideoCallHub.InviteUser` guards against `GetAllUserIdentifiers()` rather than `GetCurrentUserId()` alone, so a `sid`-addressed self-invite is rejected. Immune to ID-space drift (compares within one connection's own claims).
2. **Frontend** — align `chatService.userId` / `userIdSig` to the `sid` claim so it matches the roster identity `ChatHub` actually broadcasts. Every consumer of `chatService.userId` is hub-facing (`chat.component.ts:177,182,238`, `chat.service.ts:371`), so this also fixes DM "other participant" resolution, which is broken for the same reason.
Both are needed: the backend guard alone leaves you listed (just uninvitable); the frontend alone leaves the server unenforced.
## Related
- #98 — partial fix (race only), closed
- #99 — alias user-groups, closed; interacts to make self-call actually connect
Backend alone leaves you listed (just uninvitable); angular alone leaves the server unenforced.
Verification:
Backend: dotnet build SpikerSoft.UnitTests.slnf clean, VideoCallHubTests 20/20. The new InviteUser_WithOwnSidClaim_ShouldThrow was confirmed to fail against the old guard, so it's real coverage, not a test pinned to current behavior.
Angular: full nx test spikersoft — 3489 passed / 6 skipped. Picker + ChatService specs rewritten to sid-shaped ids; the old ones used a generic "me" on both sides of the comparison, which is why they passed while prod was broken.
Not manually QA'd against a live Keycloak session — the claim analysis comes from the hub source (ChatHub hard-requires sid to register a connection, so anyone in the roster has one). Worth a click-through after deploy.
Both halves are open — **they must land together**:
- backend: spikerj/spikersoft-backend#511 — `InviteUser` guards on `GetAllUserIdentifiers()` instead of `sub` alone
- angular: spikerj/spikersoft-angular#603 — `ChatService` keys self-identity on the `sid` claim
Backend alone leaves you listed (just uninvitable); angular alone leaves the server unenforced.
Verification:
- Backend: `dotnet build SpikerSoft.UnitTests.slnf` clean, `VideoCallHubTests` 20/20. The new `InviteUser_WithOwnSidClaim_ShouldThrow` was confirmed to **fail against the old guard**, so it's real coverage, not a test pinned to current behavior.
- Angular: full `nx test spikersoft` — 3489 passed / 6 skipped. Picker + `ChatService` specs rewritten to sid-shaped ids; the old ones used a generic `"me"` on both sides of the comparison, which is why they passed while prod was broken.
Not manually QA'd against a live Keycloak session — the claim analysis comes from the hub source (`ChatHub` hard-requires `sid` to register a connection, so anyone in the roster has one). Worth a click-through after deploy.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
The video-call picker lists the current user, and clicking yourself starts a call that rings your own browser. This is the residual root cause behind #98 — that ticket fixed the reactivity race (read
userIdSiginstead of the mutateduserIdstring), but the filter it repaired compares two different Keycloak claims, so it never matches regardless of timing.Root cause — two ID spaces
ChatHub.OnConnectedAsyncstores the roster entry withId = sid(Keycloak session id) —SpikerSoft.Contracts.SignalR/ChatHub.cs:205. The whole ofChatHubissid-keyed (DM participants, rooms, history).chatService.userId/userIdSigfromkeycloak.loadUserProfile().id, i.e. thesubuser id —_services/chat/chat.service.ts:98-99and:438-439.So
u.id (sid) !== selfId (sub)is always true and the filter is a no-op:_dialogs/video-call-picker/video-call-picker.component.ts:324-327_components/chat/chat.component.ts:64-72(same broken filter, DM list)The backend self-guard is defeated by the same skew:
And because #99's fix subscribes each connection to a user-group for every claim variant (
GetAllUserIdentifiers,VideoCallHub.cs:394-408),Clients.Group(UserGroup(calleeUserId))delivers the invite straight back to the caller's own browser. The two fixes compose into a working self-call.Why the tests didn't catch it
video-call-picker.component.spec.ts:56-61setsuserIdSig = signal("me")alongside auser("me")— ids constructed to match, so it passes whatever the real claim skew is.Fix
VideoCallHub.InviteUserguards againstGetAllUserIdentifiers()rather thanGetCurrentUserId()alone, so asid-addressed self-invite is rejected. Immune to ID-space drift (compares within one connection's own claims).chatService.userId/userIdSigto thesidclaim so it matches the roster identityChatHubactually broadcasts. Every consumer ofchatService.userIdis hub-facing (chat.component.ts:177,182,238,chat.service.ts:371), so this also fixes DM "other participant" resolution, which is broken for the same reason.Both are needed: the backend guard alone leaves you listed (just uninvitable); the frontend alone leaves the server unenforced.
Related
Both halves are open — they must land together:
InviteUserguards onGetAllUserIdentifiers()instead ofsubaloneChatServicekeys self-identity on thesidclaimBackend alone leaves you listed (just uninvitable); angular alone leaves the server unenforced.
Verification:
dotnet build SpikerSoft.UnitTests.slnfclean,VideoCallHubTests20/20. The newInviteUser_WithOwnSidClaim_ShouldThrowwas confirmed to fail against the old guard, so it's real coverage, not a test pinned to current behavior.nx test spikersoft— 3489 passed / 6 skipped. Picker +ChatServicespecs rewritten to sid-shaped ids; the old ones used a generic"me"on both sides of the comparison, which is why they passed while prod was broken.Not manually QA'd against a live Keycloak session — the claim analysis comes from the hub source (
ChatHubhard-requiressidto register a connection, so anyone in the roster has one). Worth a click-through after deploy.