From 58a9391acf0729adffec5386289d3fdbe82876fc Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 17 Oct 2026 10:50:48 +0000 Subject: [PATCH] oidc: send the change from the user update in the callback --- hscontrol/oidc.go | 4 +++- hscontrol/oidc_test.go | 52 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/hscontrol/oidc.go b/hscontrol/oidc.go index e0e98e915..7ea8285ec 100644 --- a/hscontrol/oidc.go +++ b/hscontrol/oidc.go @@ -339,7 +339,7 @@ func (a *AuthProviderOIDC) OIDCCallbackHandler( return } - user, _, err := a.createOrUpdateUserFromClaim(&claims) + user, userChange, err := a.createOrUpdateUserFromClaim(&claims) if err != nil { httpUserError(writer, NewHTTPError( http.StatusInternalServerError, @@ -350,6 +350,8 @@ func (a *AuthProviderOIDC) OIDCCallbackHandler( return } + a.h.Change(userChange) + // TODO(kradalby): Is this comment right? // If the node exists, then the node should be reauthenticated, // if the node does not exist, and the machine key exists, then diff --git a/hscontrol/oidc_test.go b/hscontrol/oidc_test.go index fd9097b72..2cb03cf11 100644 --- a/hscontrol/oidc_test.go +++ b/hscontrol/oidc_test.go @@ -17,6 +17,7 @@ import ( "github.com/oauth2-proxy/mockoidc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "tailscale.com/tailcfg" "tailscale.com/types/key" ) @@ -739,3 +740,54 @@ func TestOIDCLoginClearsUnverifiedEmail(t *testing.T) { require.NoError(t, err) assert.Empty(t, stored.Email) } + +func TestOIDCCallbackDispatchesUserIdentityChange(t *testing.T) { + b := newOIDCBrowser(t) + provider, ok := b.app.authProvider.(*AuthProviderOIDC) + require.True(t, ok) + + provider.cfg.EmailVerifiedRequired = true + user, _, err := provider.createOrUpdateUserFromClaim(&types.OIDCClaims{ + Iss: b.idp.Issuer(), Sub: "identity-change", Username: "alice", + Email: "alice@old.example", EmailVerified: true, + }) + require.NoError(t, err) + + node := b.app.state.CreateRegisteredNodeForTest(user, "identity-node") + node.User = user.Clone() + b.app.state.PutNodeInStoreForTest(*node) + require.NoError(t, b.app.state.UpdatePolicyManagerNodesForTest()) + _, err = b.app.state.SetPolicy([]byte(`{ + "acls": [{"action": "accept", "src": ["alice@old.example"], "dst": ["*:*"]}] + }`)) + require.NoError(t, err) + + responses := make(chan *tailcfg.MapResponse, 8) + require.NoError(t, b.app.mapBatcher.AddNode(node.ID, responses, tailcfg.CurrentCapabilityVersion, nil)) + t.Cleanup(func() { b.app.mapBatcher.RemoveNode(node.ID, responses) }) + + initial := <-responses + require.NotEmpty(t, initial.PacketFilters["base"]) + + b.idp.QueueUser(&mockoidc.MockUser{ + Subject: "identity-change", PreferredUsername: "alice", + Email: "alice@new.example", EmailVerified: true, + }) + _, registerURL := b.pendingNode(t) + status, _, _ := b.get(t, registerURL) + require.Equal(t, http.StatusOK, status) + + stored, err := b.app.state.GetUserByID(types.UserID(user.ID)) + require.NoError(t, err) + require.Equal(t, "alice@new.example", stored.Email) + + assert.Eventually(t, func() bool { + select { + case response := <-responses: + filters, present := response.PacketFilters["base"] + return present && len(filters) == 0 + default: + return false + } + }, 3*time.Second, 20*time.Millisecond, "the connected node must receive the updated filter before registration confirmation") +}