diff --git a/hscontrol/oidc.go b/hscontrol/oidc.go index 1a95efbbe..b69a042e1 100644 --- a/hscontrol/oidc.go +++ b/hscontrol/oidc.go @@ -881,6 +881,10 @@ func (a *AuthProviderOIDC) RegisterConfirmGetHandler( info.Hostname = regData.Hostname info.MachineKey = regData.MachineKey.ShortString() + if node, ok := a.h.state.LoginNode(regData.MachineKey, types.UserID(user.ID)); ok { + info.ExistingNode = node.GivenName() + } + if regData.Hostinfo != nil { info.OS = regData.Hostinfo.OS } @@ -985,7 +989,12 @@ func (a *AuthProviderOIDC) RegisterConfirmHandler( content = renderAuthSuccessTemplate(user) } else { - newNode, err := a.handleRegistration(user, authID, pending.NodeExpiry) + _, reauth := a.h.state.LoginNode( + authReq.RegistrationData().MachineKey, + types.UserID(user.ID), + ) + + node, err := a.handleRegistration(user, authID, pending.NodeExpiry) if err != nil { if errors.Is(err, db.ErrNodeNotFoundRegistrationCache) { httpUserError(writer, newHTTPUserError( @@ -1003,7 +1012,7 @@ func (a *AuthProviderOIDC) RegisterConfirmHandler( return } - content = renderRegistrationSuccessTemplate(user, newNode) + content = renderRegistrationSuccessTemplate(user, node, reauth) } // Clear the CSRF cookie now that the request is final. @@ -1024,8 +1033,8 @@ func (a *AuthProviderOIDC) handleRegistration( user *types.User, registrationID types.AuthID, expiry *time.Time, -) (bool, error) { - _, nodeChange, err := a.h.state.HandleNodeFromAuthPath( +) (types.NodeView, error) { + node, nodeChange, err := a.h.state.HandleNodeFromAuthPath( registrationID, types.UserID(user.ID), expiry, @@ -1034,15 +1043,16 @@ func (a *AuthProviderOIDC) handleRegistration( a.h.Change(nodeChange) if err != nil { - return false, fmt.Errorf("registering node: %w", err) + return types.NodeView{}, fmt.Errorf("registering node: %w", err) } - return !nodeChange.IsEmpty(), nil + return node, nil } func renderRegistrationSuccessTemplate( user *types.User, - newNode bool, + _ types.NodeView, + reauth bool, ) *bytes.Buffer { result := templates.AuthSuccessResult{ Title: "Headscale - Node Reauthenticated", @@ -1051,7 +1061,7 @@ func renderRegistrationSuccessTemplate( User: user.Display(), Message: "You can now close this window.", } - if newNode { + if !reauth { result.Title = "Headscale - Node Registered" result.Heading = "Node registered" result.Verb = "Registered" diff --git a/hscontrol/oidc_test.go b/hscontrol/oidc_test.go index cd2bea408..d61644554 100644 --- a/hscontrol/oidc_test.go +++ b/hscontrol/oidc_test.go @@ -951,3 +951,80 @@ func TestOIDCConfirmationRejectsWrongRequestKind(t *testing.T) { }) } } + +func TestRegisterConfirmShowsExistingNode(t *testing.T) { + b := newOIDCBrowser(t) + machineKey := key.NewMachine().Public() + existingRow := regexp.MustCompile(`>Existing node]*>([^<]*)`) + + // A tagged node another credential made: a login on its machine adds a + // device instead of taking it. + taggedMachine := key.NewMachine().Public() + tagKey, err := b.app.state.CreatePreAuthKey(nil, true, false, nil, []string{"tag:server"}) + require.NoError(t, err) + _, _, err = b.app.state.HandleNodeFromPreAuthKey(tailcfg.RegisterRequest{ + Auth: &tailcfg.RegisterResponseAuth{AuthKey: tagKey.Key}, + NodeKey: key.NewNode().Public(), + Hostinfo: &tailcfg.Hostinfo{Hostname: "server"}, + }, taggedMachine) + require.NoError(t, err) + + for _, tt := range []struct { + name string + machineKey key.MachinePublic + existing string + heading string + nodeCount int + }{ + { + name: "first login", machineKey: machineKey, + existing: "None, this is a new device", heading: "Node registered", nodeCount: 2, + }, + { + name: "same machine", machineKey: machineKey, + existing: "laptop", heading: "Node reauthenticated", nodeCount: 2, + }, + { + name: "copied hostname", machineKey: key.NewMachine().Public(), + existing: "None, this is a new device", heading: "Node registered", nodeCount: 3, + }, + { + name: "tagged machine", machineKey: taggedMachine, + existing: "None, this is a new device", heading: "Node registered", nodeCount: 4, + }, + } { + authID := types.MustAuthID() + b.app.state.SetAuthCacheEntry(authID, types.NewRegisterAuthRequest(&types.RegistrationData{ + MachineKey: tt.machineKey, + NodeKey: key.NewNode().Public(), + Hostname: "laptop", + })) + b.idp.QueueUser(&mockoidc.MockUser{ + Subject: "existing-device", PreferredUsername: "device-owner", + }) + + status, landed, body := b.get(t, b.publicURL+"/register/"+authID.String()) + require.Equal(t, http.StatusOK, status) + + row := existingRow.FindStringSubmatch(body) + if assert.Len(t, row, 2, "the page must distinguish an existing node from client-supplied details") { + assert.Equal(t, tt.existing, row[1], tt.name) + } + + csrf := csrfInputRe.FindStringSubmatch(body) + require.Len(t, csrf, 2) + + //nolint:noctx,bodyclose // test response closed below + confirmed, err := b.client.PostForm(landed.String(), url.Values{ + registerConfirmCSRFCookie: {csrf[1]}, + }) + require.NoError(t, err) + + confirmedBody, err := io.ReadAll(confirmed.Body) + confirmed.Body.Close() + require.NoError(t, err) + require.Equal(t, http.StatusOK, confirmed.StatusCode) + assert.Contains(t, string(confirmedBody), tt.heading, tt.name) + assert.Equal(t, tt.nodeCount, b.app.state.ListNodes().Len(), tt.name) + } +} diff --git a/hscontrol/templates/register_confirm.go b/hscontrol/templates/register_confirm.go index 8962cd239..d99474fdf 100644 --- a/hscontrol/templates/register_confirm.go +++ b/hscontrol/templates/register_confirm.go @@ -43,6 +43,9 @@ type RegisterConfirmInfo struct { // MachineKey is the short fingerprint of the registering machine // key. The full key is intentionally not shown. MachineKey string + + // ExistingNode is the stored node name, empty for a new device. + ExistingNode string } // RegisterConfirm renders an interstitial page that asks the @@ -57,6 +60,7 @@ func RegisterConfirm(info RegisterConfirmInfo) *elem.Element { {"Hostname", elem.Text(info.Hostname)}, {"OS", elem.Text(cmp.Or(info.OS, "(unknown)"))}, {"Machine key", Code(elem.Text(info.MachineKey))}, + {"Existing node", elem.Text(cmp.Or(info.ExistingNode, "None, this is a new device"))}, {"Registered to", elem.Text(info.User)}, }, )