[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
This commit is contained in:
Zoltan Papp
2026-08-15 11:21:57 +02:00
committed by GitHub
parent f458c1f265
commit 16544dbc58
4 changed files with 33 additions and 21 deletions
+4 -3
View File
@@ -87,9 +87,10 @@ func (pm *ProfileManager) SetActiveProfileState(state *ProfileState) error {
// RemoveProfileState deletes the per-profile state file (which holds the // RemoveProfileState deletes the per-profile state file (which holds the
// account email used for the SSO login hint and the UI display). Called after // 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 // profile removal; logout keeps the file so the next login can pass the email
// email. The state file only stores the email, so deleting it is equivalent to // as the login_hint. The state file only stores the email, so deleting it is
// clearing it; the next SSO login recreates it. A missing file is not an error. // equivalent to clearing it; the next SSO login recreates it. A missing file
// is not an error.
func (pm *ProfileManager) RemoveProfileState(profileName string) error { func (pm *ProfileManager) RemoveProfileState(profileName string) error {
configDir, err := getConfigDir() configDir, err := getConfigDir()
if err != nil { if err != nil {
+15 -3
View File
@@ -6,9 +6,11 @@ import (
"context" "context"
"time" "time"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status" gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/proto" "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 // a request from the UI implies a graphical session, which the daemon cannot detect itself
req := &proto.RequestExtendAuthSessionRequest{HasGraphicalSession: true} req := &proto.RequestExtendAuthSessionRequest{HasGraphicalSession: true}
if p.Hint != "" { hint := p.Hint
h := p.Hint if hint == "" {
req.Hint = &h 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) resp, err := cli.RequestExtendAuthSession(ctx, req)
+11 -13
View File
@@ -123,8 +123,16 @@ func (s *Connection) Login(ctx context.Context, p LoginParams) (LoginResult, err
if p.PreSharedKey != "" { if p.PreSharedKey != "" {
req.OptionalPreSharedKey = ptrStr(p.PreSharedKey) req.OptionalPreSharedKey = ptrStr(p.PreSharedKey)
} }
if p.Hint != "" { hint := p.Hint
req.Hint = ptrStr(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) resp, err := cli.Login(ctx, req)
@@ -228,16 +236,6 @@ func (s *Connection) Logout(ctx context.Context, p LogoutParams) error {
return s.classifyDaemonError(err) 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 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 // 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 // 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 // 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 — // and later logins and session extends go out without a login_hint —
// leaving the IdP to guess which account was meant. // leaving the IdP to guess which account was meant.
+3 -2
View File
@@ -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 // 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 // user-owned state file holding the account email behind. Logout keeps the
// Connection.Logout). Legacy profiles are keyed by name rather than by a // 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 // generated ID, so a recreated profile of the same name would inherit the
// deleted one's email and offer it as the login_hint. // deleted one's email and offer it as the login_hint.
// //