Files
netbird/client/ui/authsession/service.go
Zoltan Papp 16544dbc58 [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
2026-08-15 11:21:57 +02:00

130 lines
3.8 KiB
Go

//go:build !android && !ios && !freebsd && !js
package authsession
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"
)
type ExtendStartParams struct {
// Hint is the OIDC login_hint, typically the user's email.
Hint string `json:"hint"`
}
type ExtendStartResult struct {
VerificationURI string `json:"verificationUri"`
VerificationURIComplete string `json:"verificationUriComplete"`
UserCode string `json:"userCode"`
DeviceCode string `json:"deviceCode"`
ExpiresIn int64 `json:"expiresIn"`
}
type ExtendWaitParams struct {
DeviceCode string `json:"deviceCode"`
UserCode string `json:"userCode"`
}
// ExtendResult: ExpiresAt is nil when the peer is ineligible for extension.
// Preempted means a newer WaitExtend took over the IdP poll — a no-op, not a failure.
type ExtendResult struct {
ExpiresAt *time.Time `json:"sessionExpiresAt,omitempty"`
Preempted bool `json:"preempted,omitempty"`
}
// DaemonConn duplicates services.DaemonConn to avoid an import cycle.
type DaemonConn interface {
Client() (proto.DaemonServiceClient, error)
}
// Session bundles the session-auth daemon RPCs the UI drives.
type Session struct {
conn DaemonConn
}
func NewSession(conn DaemonConn) *Session {
return &Session{conn: conn}
}
// RequestExtend starts the SSO session-extension flow on the daemon.
func (s *Session) RequestExtend(ctx context.Context, p ExtendStartParams) (ExtendStartResult, error) {
cli, err := s.conn.Client()
if err != nil {
return ExtendStartResult{}, err
}
// a request from the UI implies a graphical session, which the daemon cannot detect itself
req := &proto.RequestExtendAuthSessionRequest{HasGraphicalSession: true}
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)
if err != nil {
return ExtendStartResult{}, err
}
return ExtendStartResult{
VerificationURI: resp.GetVerificationURI(),
VerificationURIComplete: resp.GetVerificationURIComplete(),
UserCode: resp.GetUserCode(),
DeviceCode: resp.GetDeviceCode(),
ExpiresIn: resp.GetExpiresIn(),
}, nil
}
// WaitExtend blocks until the user completes the SSO flow started by RequestExtend.
func (s *Session) WaitExtend(ctx context.Context, p ExtendWaitParams) (ExtendResult, error) {
cli, err := s.conn.Client()
if err != nil {
return ExtendResult{}, err
}
resp, err := cli.WaitExtendAuthSession(ctx, &proto.WaitExtendAuthSessionRequest{
DeviceCode: p.DeviceCode,
UserCode: p.UserCode,
})
if err != nil {
if st, ok := gstatus.FromError(err); ok && st.Code() == codes.Canceled {
return ExtendResult{Preempted: true}, nil
}
return ExtendResult{}, err
}
out := ExtendResult{}
if ts := resp.GetSessionExpiresAt(); ts.IsValid() && !ts.AsTime().IsZero() {
t := ts.AsTime().UTC()
out.ExpiresAt = &t
}
return out, nil
}
// DismissWarning suppresses the daemon's T-FinalWarningLead fallback dialog for
// the current deadline. Best-effort: a stale call is silently swallowed daemon-side.
func (s *Session) DismissWarning(ctx context.Context) error {
cli, err := s.conn.Client()
if err != nil {
return err
}
_, err = cli.DismissSessionWarning(ctx, &proto.DismissSessionWarningRequest{})
return err
}