mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-25 00:51:28 +02:00
The Android binding never recorded which account a profile belongs to, so every interactive login and every session extend went to the IdP with no login_hint. With nothing to go on the IdP picks an account itself, which on a session extend means re-authenticating an account the profile is already signed in with. Store the email the PKCE flow already parses out of the ID token, and pass it back as the hint on later flows. An empty hint stays meaningful: a fresh profile, or one that was logged out, deliberately leaves the choice to the IdP, which is how a profile changes accounts. Logout clears the stored email for that reason — while it is on disk it would steer the next login straight back into the account just logged out of. The email is keyed off the profile's config path rather than the active profile: Auth.login runs in a goroutine, so the active profile can change under a flow already in flight. It lands in <profile>.account.json, not the <profile>.state.json desktop uses for the same data — there the email and the engine's state manager sit in different directories, but on Android both resolve under files/, and the state manager rewrites the whole file from its own keys.
109 lines
3.7 KiB
Go
109 lines
3.7 KiB
Go
package android
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
|
"github.com/netbirdio/netbird/util"
|
|
)
|
|
|
|
const (
|
|
// Android-specific config filename (different from desktop default.json)
|
|
defaultConfigFilename = "netbird.cfg"
|
|
// Subdirectory for non-default profiles (must match Java Preferences.java)
|
|
profilesSubdir = "profiles"
|
|
// profileAccountSuffix names the file holding the profile's account email.
|
|
// Deliberately not ".state.json", which desktop uses for the same data:
|
|
// there the email and the engine's state manager live in different
|
|
// directories, but on Android both resolve under files/, so sharing the name
|
|
// would have the two overwrite each other — the state manager rewrites the
|
|
// whole file from its own keys (see statemanager.Manager.PersistState), and
|
|
// this package's writer does the same in reverse.
|
|
profileAccountSuffix = ".account.json"
|
|
)
|
|
|
|
// profileAccountPathFor derives the account file path from a profile's config
|
|
// path: netbird.cfg -> netbird.account.json, <id>.json -> <id>.account.json.
|
|
//
|
|
// Deriving from the config path rather than resolving the active profile keeps
|
|
// the write on the profile the login actually ran for: Auth.login runs in a
|
|
// goroutine, so the active profile can change under a flow already in flight.
|
|
func profileAccountPathFor(configPath string) (string, error) {
|
|
if configPath == "" {
|
|
return "", fmt.Errorf("empty config path")
|
|
}
|
|
|
|
base := filepath.Base(configPath)
|
|
stem := strings.TrimSuffix(base, filepath.Ext(base))
|
|
if stem == "" || stem == "." {
|
|
return "", fmt.Errorf("config path %q has no filename stem", configPath)
|
|
}
|
|
|
|
return filepath.Join(filepath.Dir(configPath), stem+profileAccountSuffix), nil
|
|
}
|
|
|
|
// readProfileEmail returns the account email stored for the profile whose config
|
|
// lives at configPath. A missing or unreadable file yields "", which leaves the
|
|
// account choice to the IdP.
|
|
func readProfileEmail(configPath string) string {
|
|
accountPath, err := profileAccountPathFor(configPath)
|
|
if err != nil {
|
|
log.Debugf("no profile account path for login hint: %v", err)
|
|
return ""
|
|
}
|
|
|
|
var state profilemanager.ProfileState
|
|
if _, err := util.ReadJson(accountPath, &state); err != nil {
|
|
if !os.IsNotExist(err) {
|
|
log.Debugf("failed to read profile account for login hint: %v", err)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
return state.Email
|
|
}
|
|
|
|
// writeProfileEmail records the account email for the profile whose config lives
|
|
// at configPath, so later logins can pass it as an OIDC login_hint. An empty
|
|
// email is ignored rather than blanking what is already stored.
|
|
func writeProfileEmail(configPath string, email string) error {
|
|
if email == "" {
|
|
return nil
|
|
}
|
|
|
|
accountPath, err := profileAccountPathFor(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve profile account path: %w", err)
|
|
}
|
|
|
|
state := profilemanager.ProfileState{Email: email}
|
|
if err := util.WriteJsonWithRestrictedPermission(context.Background(), accountPath, state); err != nil {
|
|
return fmt.Errorf("write profile account: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// removeProfileEmail drops the stored account email. Called on logout: while the
|
|
// email is on disk it goes out as a login_hint, which would steer the next login
|
|
// straight back into the account just logged out of. Mirrors the desktop UI's
|
|
// RemoveProfileState call.
|
|
func removeProfileEmail(configPath string) error {
|
|
accountPath, err := profileAccountPathFor(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve profile account path: %w", err)
|
|
}
|
|
|
|
if err := os.Remove(accountPath); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("remove profile account: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|