From 16544dbc584ff4f8d76ef66fa67b1a9c028d91a9 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Sat, 15 Aug 2026 09:21:57 +0000 Subject: [PATCH] [client] Pass stored email as login hint from UI and keep it on logout (#7199) * [client] Pass stored email as login hint from UI and keep it on logout Follow the CLI pattern: the Wails UI now reads the account email from the user-owned profile state file and passes it as the OIDC login_hint on login and session extend, since the daemon-side fallback runs as root and cannot see the user's state file. Logout no longer deletes the stored email, so a later login preselects the account at the IdP; profile removal remains the operation that deletes it. * [client] Log ignored profile lookup errors in extend-session hint fallback --- client/internal/profilemanager/state.go | 7 ++++--- client/ui/authsession/service.go | 18 +++++++++++++++--- client/ui/services/connection.go | 24 +++++++++++------------- client/ui/services/profile.go | 5 +++-- 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/client/internal/profilemanager/state.go b/client/internal/profilemanager/state.go index fcd1c384c..ddb5dd056 100644 --- a/client/internal/profilemanager/state.go +++ b/client/internal/profilemanager/state.go @@ -87,9 +87,10 @@ func (pm *ProfileManager) SetActiveProfileState(state *ProfileState) error { // RemoveProfileState deletes the per-profile state file (which holds the // account email used for the SSO login hint and the UI display). Called after -// a successful logout so a logged-out profile no longer shows a stale account -// email. The state file only stores the email, so deleting it is equivalent to -// clearing it; the next SSO login recreates it. A missing file is not an error. +// profile removal; logout keeps the file so the next login can pass the email +// as the login_hint. The state file only stores the email, so deleting it is +// equivalent to clearing it; the next SSO login recreates it. A missing file +// is not an error. func (pm *ProfileManager) RemoveProfileState(profileName string) error { configDir, err := getConfigDir() if err != nil { diff --git a/client/ui/authsession/service.go b/client/ui/authsession/service.go index d94cef696..9c094c2a1 100644 --- a/client/ui/authsession/service.go +++ b/client/ui/authsession/service.go @@ -6,9 +6,11 @@ import ( "context" "time" + log "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" gstatus "google.golang.org/grpc/status" + "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/proto" ) @@ -60,9 +62,19 @@ func (s *Session) RequestExtend(ctx context.Context, p ExtendStartParams) (Exten // a request from the UI implies a graphical session, which the daemon cannot detect itself req := &proto.RequestExtendAuthSessionRequest{HasGraphicalSession: true} - if p.Hint != "" { - h := p.Hint - req.Hint = &h + hint := p.Hint + if hint == "" { + pm := profilemanager.NewProfileManager() + if active, perr := pm.GetActiveProfile(); perr != nil { + log.Debugf("failed to get active profile for login hint: %v", perr) + } else if state, serr := pm.GetProfileState(active.ID); serr != nil { + log.Debugf("failed to get profile state for login hint: %v", serr) + } else { + hint = state.Email + } + } + if hint != "" { + req.Hint = &hint } resp, err := cli.RequestExtendAuthSession(ctx, req) diff --git a/client/ui/services/connection.go b/client/ui/services/connection.go index aa649bb6d..f78ce4c0f 100644 --- a/client/ui/services/connection.go +++ b/client/ui/services/connection.go @@ -123,8 +123,16 @@ func (s *Connection) Login(ctx context.Context, p LoginParams) (LoginResult, err if p.PreSharedKey != "" { req.OptionalPreSharedKey = ptrStr(p.PreSharedKey) } - if p.Hint != "" { - req.Hint = ptrStr(p.Hint) + hint := p.Hint + if hint == "" && profileID != "" { + if state, serr := profilemanager.NewProfileManager().GetProfileState(profilemanager.ID(profileID)); serr == nil { + hint = state.Email + } else { + log.Debugf("failed to get profile state for login hint: %v", serr) + } + } + if hint != "" { + req.Hint = ptrStr(hint) } resp, err := cli.Login(ctx, req) @@ -228,16 +236,6 @@ func (s *Connection) Logout(ctx context.Context, p LogoutParams) error { return s.classifyDaemonError(err) } - // The daemon runs as root and can't reach the user-owned per-profile state - // file holding the account email (see Profiles.List), so clear the stale - // email here; the next SSO login recreates it. - if p.ProfileName != "" { - if err := profilemanager.NewProfileManager().RemoveProfileState(p.ProfileName); err != nil { - // Non-fatal: the logout itself succeeded. - log.Warnf("failed to remove profile state for %s: %v", p.ProfileName, err) - } - } - return nil } @@ -261,7 +259,7 @@ func (s *Connection) waitSSOLogin(ctx context.Context, p WaitSSOParams) (string, // Persist the account email the same way the CLI does after its own // WaitSSOLogin: the daemon returns it but cannot store it, since it runs as - // root and the per-profile state file is user-owned (see Logout below). + // root and the per-profile state file is user-owned (see Profiles.List). // Without this the profile has no email, so Profiles.List shows no account // and later logins and session extends go out without a login_hint — // leaving the IdP to guess which account was meant. diff --git a/client/ui/services/profile.go b/client/ui/services/profile.go index 5a9a0e68d..e76ab3db6 100644 --- a/client/ui/services/profile.go +++ b/client/ui/services/profile.go @@ -162,8 +162,9 @@ func (s *Profiles) Remove(ctx context.Context, p ProfileRef) error { } // The daemon deletes what it owns but runs as root, so it leaves the - // user-owned state file holding the account email behind (same split as - // Connection.Logout). Legacy profiles are keyed by name rather than by a + // user-owned state file holding the account email behind. Logout keeps the + // email on purpose so later logins can pass it as the login_hint; profile + // removal is what deletes it. Legacy profiles are keyed by name rather than by a // generated ID, so a recreated profile of the same name would inherit the // deleted one's email and offer it as the login_hint. //